diff --git a/.claude/skills/kdoc/SKILL.md b/.claude/skills/kdoc/SKILL.md new file mode 100644 index 0000000..cfacfc3 --- /dev/null +++ b/.claude/skills/kdoc/SKILL.md @@ -0,0 +1,81 @@ +--- +name: kdoc +description: "Add KDoc documentation to Kotlin public API. Use whenever the user asks to document Kotlin code, add KDoc, generate API docs, mentions undocumented public declarations, or wants to improve existing documentation. Also trigger when the user says 'add docs', 'document this class/file/module', 'write KDoc', or asks about missing documentation in Kotlin code." +--- + +# KDoc Generator + +Add KDoc to all public Kotlin declarations (classes, interfaces, objects, functions, properties, enums, type aliases, annotations — including `@Deprecated`). Read the implementation, not just the signature, to write accurate descriptions. + +## Tag placement + +| Where the declaration lives | Tag | +|---|---| +| Primary-ctor `val` / `var` (public, internal, or protected) | `@property` in class KDoc | +| Primary-ctor `name: Type` (no `val`/`var`) | `@param` in class KDoc | +| Primary-ctor `private val` / `private var` | `@param` in class KDoc | +| Body of class/object/interface/sealed interface | inline `/** */` above the declaration | +| Function parameter | `@param` | +| Non-Unit return type | `@return` | + +## Style + +- Summary: one concise sentence, third-person verb ("Creates", "Returns", "Represents"). Expand to 2-3 sentences only when genuinely complex. +- **Example block**: mandatory for DSL builders (show full receiver-scope usage); add for complex functions or non-obvious extension functions; skip for one-liners and simple getters. +- KDoc links (`[ClassName]`, `[methodName]`) only where they add navigational value. +- Don't write `@throws` or "suspend" notes. +- Rewrite existing KDoc if incomplete or low quality. + +## Examples + +**Class with mixed primary-constructor parameters:** + +````kotlin +/** + * Manages active client sessions and their lifecycle. + * + * @property maxSessions upper limit on concurrent sessions + * @param scope coroutine scope for session lifecycle tasks + * @param secret used internally for session token signing + */ +public class SessionManager( + public val maxSessions: Int, // public val → @property + scope: CoroutineScope, // no val/var → @param + private val secret: ByteArray, // private val → @param +) +```` + +**Body and interface properties (inline `/** */`):** + +````kotlin +/** Represents an entity that includes additional metadata. */ +public interface WithMeta { + /** Optional metadata attached to this entity. */ + public val meta: JsonObject? +} + +public class Connection(public val id: String) { + /** Capabilities negotiated during the handshake, or `null` before initialize completes. */ + public val capabilities: ServerCapabilities? get() = ... +} +```` + +**Function with Example block:** + +````kotlin +/** + * Registers a new tool with the given handler. + * + * Example: + * ```kotlin + * server.addTool(name = "echo", description = "Echoes input back") { request -> + * CallToolResult(content = listOf(TextContent("Echo: ${request.arguments}"))) + * } + * ``` + * + * @param name unique tool identifier + * @param description human-readable tool description + * @param handler suspend function invoked when the tool is called + * @return the registered tool definition + */ +```` diff --git a/.claude/skills/mcp-docs/SKILL.md b/.claude/skills/mcp-docs/SKILL.md new file mode 100644 index 0000000..074e6f9 --- /dev/null +++ b/.claude/skills/mcp-docs/SKILL.md @@ -0,0 +1,76 @@ +--- +name: mcp-docs +description: > + Fetch live MCP specification and docs from modelcontextprotocol.io. Training data may be + outdated — default to using this skill when the query touches MCP protocol in any way. + Trigger for: implementing or debugging MCP features (sampling, elicitation, completion, tools, + resources, prompts, transports); SEPs or spec requirements; MCP transport behavior (SSE, + Streamable HTTP, reconnection, retry); authorization/OAuth; conformance test failures; + capability negotiation; message schemas. Skip only for pure Kotlin, build system, or + refactoring tasks with zero MCP protocol dependency. When in doubt, trigger. +user_invocable: true +--- + +# MCP Documentation Skill + +Provides access to the official MCP specification and docs from modelcontextprotocol.io. The spec evolves faster than training data — always consult live documentation for protocol specifics like message schemas, required fields, capability negotiation, error handling, and transport requirements. + +## When to use + +**Proactively** (without being asked): +- Implementing a feature defined in the MCP spec +- Reviewing code that implements MCP protocol behavior +- Fixing conformance test failures + +**On demand** (user asks or invokes `/mcp-docs`): +- Questions about how a specific MCP feature works +- Clarifying spec ambiguities +- Looking up SEPs (Specification Enhancement Proposals) + +## How to fetch documentation + +### Strategy: MCP tool first, llms.txt fallback + +**Step 1 — Try the MCP search tool.** If `search_model_context_protocol` is available, use it: + +``` +search_model_context_protocol(query: "") +``` + +One call, semantically ranked results. Fastest path. + +**Step 2 — Fallback to llms.txt + WebFetch.** If the MCP tool is not available: + +1. Fetch the documentation index: + ``` + WebFetch("https://modelcontextprotocol.io/llms.txt", "Return the full content as-is") + ``` + +2. Identify the relevant page URL(s) from the index. Key sections: + - **Specification**: `specification/...` — protocol details, message formats, transports, lifecycle + - **SEPs**: `seps/...` — specification enhancement proposals + - **Learning**: `docs/learn/...` — architecture, concepts + - **Development**: `docs/develop/...` — building clients/servers + +3. Fetch the specific page: + ``` + WebFetch("", "Return the full content of this documentation page") + ``` + +### Version handling + +Always use the latest spec version by default. Only use a specific version or draft when the user explicitly requests it (e.g., "check the 2025-03-26 spec" or "what does the draft say"). + +### Search guidance + +- Protocol message formats, required fields, capability negotiation → **Specification** section +- Feature motivation, design rationale → relevant **SEP** +- High-level concepts and architecture → **Learning** resources +- Use your judgment on query count. A field-level question may need one page; implementing a feature may need 2-3. + +## Presenting information + +- **Always cite sources.** Include links to spec pages used: `(source: )` +- **Spec over training data** for specific protocol details (schemas, fields, error codes, capabilities). Training data is fine for general MCP concepts. +- **Preserve RFC 2119 language.** The spec uses MUST/SHOULD/MAY — keep this distinction when explaining requirements. +- **Be concise.** Focus on the details relevant to the user's task. Avoid dumping entire spec pages — extract what matters for the specific question or implementation at hand. diff --git a/.editorconfig b/.editorconfig index 5c283ed..d8964b9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -30,6 +30,7 @@ ktlint_class_signature_rule_force_multiline_when_parameter_count_greater_or_equa ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 4 ktlint_standard_chain-method-continuation = disabled ktlint_ignore_back_ticked_identifier = true +ktlint_standard_backing-property-naming = disabled ktlint_standard_multiline-expression-wrapping = disabled ktlint_standard_when-entry-bracing = disabled diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8d02aac..b951eaa 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,20 +1,109 @@ -# Configuration options: -# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#about-the-dependabotyml-file +# yaml-language-server: $schema=https://json.schemastore.org/dependabot-2.0.json +# Configuration: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 + updates: + # Root Gradle project (includes buildSrc) - package-ecosystem: "gradle" directory: "/" schedule: interval: "weekly" + day: "monday" + commit-message: + prefix: "chore(deps):" labels: - "dependencies" - "kotlin" + open-pull-requests-limit: 10 + groups: + ktor: + patterns: + - "io.ktor:*" + kotlinx: + patterns: + - "org.jetbrains.kotlinx:*" + kotlin: + patterns: + - "org.jetbrains.kotlin:*" + kotest: + patterns: + - "io.kotest:*" + testing: + patterns: + - "io.mockk:*" + - "dev.mokksy:*" + - "org.awaitility:*" + - "org.junit.jupiter:*" + infrastructure: + patterns: + - "io.netty:*" + - "io.github.oshai:*" + - "org.slf4j:*" + gradle-plugins: + patterns: + - "dev.detekt" + - "org.jetbrains.kotlinx.kover" + - "org.jlleitschuh.gradle.ktlint" + - "org.jetbrains.kotlinx.knit" + - "org.jetbrains.kotlinx.binary-compatibility-validator" + - "org.openapi.generator" + - "org.jetbrains.dokka:*" + - "com.vanniktech:*" + # Samples (independent Gradle builds) + - package-ecosystem: "gradle" + directories: + - "/samples/*" + schedule: + interval: "weekly" + day: "monday" + commit-message: + prefix: "chore(deps):" + labels: + - "dependencies" + - "kotlin" + - "samples" + open-pull-requests-limit: 10 + groups: + other-dependencies: + patterns: + - "*" + exclude-patterns: + - "io.ktor*" + - "org.jetbrains.kotlin*" + - "io.modelcontextprotocol*" + + # npm (integration tests) + - package-ecosystem: "npm" + directory: "/integration-test/src/jvmTest/typescript" + schedule: + interval: "weekly" + day: "monday" + commit-message: + prefix: "chore(deps):" + labels: + - "dependencies" + - "javascript" + open-pull-requests-limit: 10 + groups: + all-dependencies: + patterns: + - "*" + + # GitHub Actions - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" + day: "monday" + commit-message: + prefix: "chore(deps):" labels: - "dependencies" - "github-actions" + open-pull-requests-limit: 10 + groups: + all-actions: + patterns: + - "*" diff --git a/.github/workflows/apidocs.yaml b/.github/workflows/apidocs.yaml index 60a6e29..67770a7 100644 --- a/.github/workflows/apidocs.yaml +++ b/.github/workflows/apidocs.yaml @@ -1,4 +1,4 @@ -name: Build and Publish API Docs to GitHub Pages +name: Publish API Docs on: release: @@ -7,6 +7,10 @@ on: # Allow running this workflow manually from the Actions tab workflow_dispatch: +env: + JAVA_VERSION: 21 + JAVA_DISTRIBUTION: temurin + jobs: deploy: @@ -20,30 +24,27 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 - with: - submodules: true # Fetch Hugo themes (true OR recursive) - fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod + - uses: actions/checkout@v7 - - name: Set up JDK 21 - uses: actions/setup-java@v5 + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v6 with: - java-version: '21' - distribution: 'temurin' + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DISTRIBUTION }} cache: gradle - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v6.3.0 - name: Generate Dokka Site run: |- - ./gradlew clean dokkaGenerate + ./gradlew knit docs:classes docs:knitCheck dokkaGenerate - name: Upload artifact - uses: actions/upload-pages-artifact@v4 + uses: actions/upload-pages-artifact@v5 with: path: build/dokka/html - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/automations.yml b/.github/workflows/automations.yml new file mode 100644 index 0000000..60a9d3b --- /dev/null +++ b/.github/workflows/automations.yml @@ -0,0 +1,103 @@ +name: Automations + +on: + schedule: + # Runs every day at 2:00 AM UTC + - cron: '0 2 * * *' + workflow_dispatch: + inputs: + DRY_RUN: + type: boolean + description: 'Run in dry run mode' + default: true + +permissions: + actions: write + # contents: write # only for delete-branch option + issues: write + pull-requests: write + +env: + DRY_RUN: true + MOVE_TO_STALE_DAYS: 30 + CLOSE_AFTER_STALE_DAYS: 15 + +jobs: + mark-stale: + name: Mark/unmark stale PRs and issues + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v11 + with: + debug-only: ${{ env.DRY_RUN }} + exempt-draft-pr: true + exempt-issue-labels: 'awaiting-approval,work-in-progress' + stale-issue-label: 'stale' + stale-pr-label: 'stale' + + # --- Timing --- + # Job 1 marks/unmarks, but never closes. + days-before-issue-stale: ${{ env.MOVE_TO_STALE_DAYS }} + days-before-issue-close: -1 + days-before-pr-stale: ${{ env.MOVE_TO_STALE_DAYS }} + days-before-pr-close: -1 + + remove-stale-when-updated: true + remove-pr-stale-when-updated: true + + # --- Messages --- + stale-issue-message: > + 👋 **This issue has been marked as Stale.** + + This issue has had no activity for **${{ env.MOVE_TO_STALE_DAYS }} days**. + + — 🤖 Stale Bot + stale-pr-message: > + 👋 **This PR has been marked as Stale.** + + This PR has had no activity for **${{ env.MOVE_TO_STALE_DAYS }} days**. + + — 🤖 Stale Bot + + operations-per-run: 100 + ascending: true + + close-stale: + name: Close stale PRs and issues + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v11 + with: + debug-only: ${{ env.DRY_RUN }} + only-labels: 'stale' + stale-issue-label: 'stale' + stale-pr-label: 'stale' + + # --- Timing --- + # Job 2 only closes already marked items. + days-before-issue-stale: -1 + days-before-pr-stale: -1 + days-before-issue-close: ${{ env.CLOSE_AFTER_STALE_DAYS }} + days-before-pr-close: ${{ env.CLOSE_AFTER_STALE_DAYS }} + + # --- Messages --- + close-issue-message: > + 👋 **This issue has been automatically closed.** + + This issue was labeled with `stale` and has had no activity + for **${{ env.CLOSE_AFTER_STALE_DAYS }} days** since that. + + — 🤖 Stale Bot + close-pr-message: > + 👋 **This PR has been automatically closed.** + + This PR was labeled with `stale` and has had no activity + for **${{ env.CLOSE_AFTER_STALE_DAYS }} days** since that. + + If this is still relevant, please feel free to reopen it + or create a new PR. + + — 🤖 Stale Bot + + operations-per-run: 100 + ascending: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 20b9a7f..ff99581 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,68 +10,202 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} # Cancel only when the run is NOT on `main` branch - cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +permissions: + contents: read + +env: + JAVA_VERSION: 21 + JAVA_DISTRIBUTION: temurin + GRADLE_OPTS: "-Dorg.gradle.daemon=false" jobs: - validate-pr: - runs-on: macos-latest-xlarge - name: Validate PR - env: - JAVA_OPTS: "-Xmx8g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6g" - steps: - - uses: actions/checkout@v5 + build: + runs-on: ${{ matrix.os }} + name: Build (${{ matrix.job-name }}) + timeout-minutes: 30 + permissions: + contents: read - - name: Set up JDK 21 - uses: actions/setup-java@v5 + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + job-name: "Linux Tests" + gradle-tasks: "build" + max-workers: 3 + + - os: macos-latest + job-name: "iOS Simulator tests" + gradle-tasks: "iosSimulatorArm64Test" + max-workers: 2 + + steps: + - uses: actions/checkout@v7 + + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v6 with: - java-version: '21' - distribution: 'temurin' + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DISTRIBUTION }} + + - name: Set up Node.js + if: matrix.os == 'ubuntu-latest' + uses: actions/setup-node@v7 + with: + node-version: '22' # increase only after https://github.com/nodejs/node/issues/56645 will be fixed + + - name: Set up Node Packages + if: matrix.os == 'ubuntu-latest' + working-directory: integration-test/src/jvmTest/typescript + run: npm ci - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v6.3.0 with: + validate-wrappers: true add-job-summary: 'always' - cache-read-only: true + cache-read-only: ${{ github.ref != 'refs/heads/main' }} + gradle-home-cache-includes: | + caches + notifications + ../.konan/** - - name: Build with Gradle + - name: ${{ matrix.job-name }} run: |- - ./gradlew clean ktlintCheck build koverLog koverHtmlReport - ./gradlew :kotlin-sdk-core:publishToMavenLocal :kotlin-sdk-client:publishToMavenLocal :kotlin-sdk-server:publishToMavenLocal + ./gradlew ${{ matrix.gradle-tasks }} --max-workers ${{ matrix.max-workers }} --continue --rerun-tasks - - name: Build Kotlin-MCP-Client Sample - working-directory: ./samples/kotlin-mcp-client - run: ./../../gradlew clean build + - name: Coverage Report + if: ${{ success() && matrix.os == 'ubuntu-latest' }} + run: ./gradlew koverLog koverHtmlReport koverXmlReport - - name: Build Kotlin-MCP-Server Sample - working-directory: ./samples/kotlin-mcp-server - run: ./../../gradlew clean build - - - name: Build Weather-Stdio-Server Sample - working-directory: ./samples/weather-stdio-server - run: ./../../gradlew clean build + - name: Publish to Maven Local + if: ${{ success() && matrix.os == 'ubuntu-latest' }} + run: ./gradlew publishToMavenLocal -Pversion=1-SNAPSHOT - name: Upload Reports - if: ${{ !cancelled() }} - uses: actions/upload-artifact@v4 + if: failure() + uses: actions/upload-artifact@v7 with: - name: reports + name: reports-build-${{ matrix.os }} path: | **/build/reports/ - name: Publish Test Report - uses: mikepenz/action-junit-report@v5 - if: ${{ !cancelled() }} # always run even if the previous step fails + uses: mikepenz/action-junit-report@v6 + if: always() with: - report_paths: '**/test-results/**/TEST-*.xml' - detailed_summary: true - flaky_summary: true - include_empty_in_summary: false - include_time_in_summary: true annotate_only: true + detailed_summary: true + group_suite: true + include_empty_in_summary: false + report_paths: '**/test-results/**/TEST-*.xml' + truncate_stack_traces: false - - name: Disable Auto-Merge on Fail - if: failure() && github.event_name == 'pull_request' - run: gh pr merge --disable-auto "$PR_URL" - env: - PR_URL: ${{github.event.pull_request.html_url}} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Archive Maven Local Repository + if: ${{ success() && matrix.os == 'ubuntu-latest' }} + run: | + cd ~/.m2/repository/io/modelcontextprotocol && zip -r $GITHUB_WORKSPACE/local-maven-repository.zip . + + - name: Upload Maven Local Repository + if: ${{ success() && matrix.os == 'ubuntu-latest' }} + uses: actions/upload-artifact@v7 + with: + name: local-maven-repository + path: local-maven-repository.zip + retention-days: 1 + + knit: + runs-on: ubuntu-latest + name: Knit + timeout-minutes: 10 + permissions: + contents: read + + steps: + - uses: actions/checkout@v7 + + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v6 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DISTRIBUTION }} + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6.3.0 + with: + validate-wrappers: true + add-job-summary: 'always' + cache-read-only: ${{ github.ref != 'refs/heads/main' }} + gradle-home-cache-includes: | + caches + notifications + sdks + ../.konan/** + + - name: Run Knit + run: |- + ./gradlew knit docs:classes docs:knitCheck --max-workers 3 --continue --rerun-tasks + + + samples: + needs: [ build ] + runs-on: ubuntu-latest + permissions: + contents: read + strategy: + fail-fast: false + matrix: + sample: + - kotlin-mcp-client + - kotlin-mcp-server + - simple-streamable-server + - weather-stdio-server + + name: "Build Sample: ${{ matrix.sample }}" + timeout-minutes: 10 + steps: + - uses: actions/checkout@v7 + + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v6 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DISTRIBUTION }} + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6.3.0 + with: + add-job-summary: 'always' + cache-read-only: true + gradle-home-cache-includes: | + caches + notifications + sdks + ../.konan/** + + - name: Download Maven Local Repository + uses: actions/download-artifact@v8 + with: + name: local-maven-repository + path: . + + - name: Extract Maven Local Repository + run: | + mkdir -p ~/.m2/repository/io/modelcontextprotocol + unzip -o local-maven-repository.zip -d ~/.m2/repository/io/modelcontextprotocol + + - name: "Build Sample: ${{ matrix.sample }}" + working-directory: ./samples/${{ matrix.sample }} + run: ./gradlew build -Pmcp.kotlin.overrideVersion=1-SNAPSHOT --rerun-tasks + + - name: Upload Reports + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v7 + with: + name: reports-samples-snapshot-${{ matrix.sample }} + path: | + **/build/reports/ diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 63c798f..2e7c58f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,69 +1,57 @@ name: "CodeQL Advanced" on: - push: - branches: [ "main" ] pull_request: branches: [ "main" ] schedule: - - cron: '0 4 * * 0' + - cron: '0 7 * * 1' # Monday 8:00 CET (7:00 UTC) + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + JAVA_VERSION: 21 + JAVA_DISTRIBUTION: temurin jobs: analyze: - name: Analyze (${{ matrix.language }}) + name: Analyze runs-on: ubuntu-latest + timeout-minutes: 30 permissions: - # required for all workflows security-events: write - - # required to fetch internal or private CodeQL packs - packages: read - - # only required for workflows in private repositories - actions: read contents: read - strategy: - matrix: - language: [ java-kotlin ] - steps: - name: Checkout repository - uses: actions/checkout@v5 - with: - fetch-depth: 0 + uses: actions/checkout@v7 - - uses: actions/setup-java@v5 + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v6 with: - distribution: temurin - java-version: '21' + distribution: ${{ env.JAVA_DISTRIBUTION }} + java-version: ${{ env.JAVA_VERSION }} - # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4.38.0 with: - languages: ${{ matrix.language }} + languages: java-kotlin build-mode: manual - - uses: actions/cache@v4 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6.3.0 with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + validate-wrappers: true + cache-read-only: true - name: Build Kotlin sources run: | - ./gradlew \ - :kotlin-sdk-core:compileKotlinJvm \ - :kotlin-sdk-client:compileKotlinJvm \ - :kotlin-sdk-server:compileKotlinJvm \ - :kotlin-sdk:compileKotlinJvm \ - :kotlin-sdk-test:compileKotlinJvm \ + ./gradlew compileKotlinJvm :conformance-test:compileKotlin \ -Pkotlin.incremental=false \ - --no-daemon --stacktrace --rerun-tasks + --no-daemon --rerun-tasks --stacktrace - name: Analyze - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4.38.0 with: - category: '/language:${{ matrix.language }}' + category: '/language:java-kotlin' diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml new file mode 100644 index 0000000..b3e3935 --- /dev/null +++ b/.github/workflows/conformance.yml @@ -0,0 +1,172 @@ +name: Conformance Tests + +on: + workflow_dispatch: + pull_request: + branches: [ main ] + push: + branches: [ main ] + +env: + JAVA_VERSION: '21' + JAVA_DISTRIBUTION: temurin + NODE_VERSION: '22' + + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + # Cancel only when the run is NOT on `main` branch + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + server: + runs-on: ubuntu-latest + name: Conformance Server Tests + timeout-minutes: 20 + + steps: + - uses: actions/checkout@v7 + + - name: Set up JDK + uses: actions/setup-java@v6 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DISTRIBUTION }} + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6.3.0 + with: + cache-read-only: ${{ github.ref != 'refs/heads/main' }} + + - name: Build + run: ./gradlew :conformance-test:installDist + + - name: Start server + run: | + MCP_PORT=3001 conformance-test/build/install/conformance-test/bin/conformance-test & + for i in $(seq 1 30); do + if curl -s -o /dev/null http://localhost:3001/mcp; then + echo "Server is ready" + break + fi + sleep 1 + done + + - name: Run conformance tests + uses: modelcontextprotocol/conformance@v0.1.16 + with: + mode: server + url: http://localhost:3001/mcp + suite: active + node-version: ${{ env.NODE_VERSION }} + expected-failures: ./conformance-test/conformance-baseline.yml + + client: + runs-on: ubuntu-latest + name: "Conformance Client Tests: ${{ matrix.scenario }}" + timeout-minutes: 20 + + strategy: + fail-fast: false + matrix: + scenario: + - initialize + - tools_call + - elicitation-sep1034-client-defaults + - sse-retry + + steps: + - uses: actions/checkout@v7 + + - name: Set up JDK + uses: actions/setup-java@v6 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DISTRIBUTION }} + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6.3.0 + with: + cache-read-only: ${{ github.ref != 'refs/heads/main' }} + + - name: Build + run: ./gradlew :conformance-test:installDist + + - name: Run conformance tests + uses: modelcontextprotocol/conformance@v0.1.16 + with: + mode: client + command: conformance-test/build/install/conformance-test/bin/conformance-client + scenario: ${{ matrix.scenario }} + node-version: ${{ env.NODE_VERSION }} + expected-failures: ./conformance-test/conformance-baseline.yml + + auth: + runs-on: ubuntu-latest + name: Conformance Auth Tests + timeout-minutes: 20 + + steps: + - uses: actions/checkout@v7 + + - name: Set up JDK + uses: actions/setup-java@v6 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DISTRIBUTION }} + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6.3.0 + with: + cache-read-only: ${{ github.ref != 'refs/heads/main' }} + + - name: Build + run: ./gradlew :conformance-test:installDist + + - name: Run conformance tests + uses: modelcontextprotocol/conformance@v0.1.16 + with: + mode: client + command: conformance-test/build/install/conformance-test/bin/conformance-client + suite: auth + node-version: ${{ env.NODE_VERSION }} + expected-failures: ./conformance-test/conformance-baseline.yml + + auth-scenarios: + runs-on: ubuntu-latest + name: "Conformance Auth Scenario: ${{ matrix.scenario }}" + timeout-minutes: 20 + + strategy: + fail-fast: false + matrix: + scenario: + - auth/client-credentials-jwt + - auth/client-credentials-basic + - auth/cross-app-access-complete-flow + + steps: + - uses: actions/checkout@v7 + + - name: Set up JDK + uses: actions/setup-java@v6 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DISTRIBUTION }} + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6.3.0 + with: + cache-read-only: ${{ github.ref != 'refs/heads/main' }} + + - name: Build + run: ./gradlew :conformance-test:installDist + + - name: Run conformance tests + uses: modelcontextprotocol/conformance@v0.1.16 + with: + mode: client + command: conformance-test/build/install/conformance-test/bin/conformance-client + scenario: ${{ matrix.scenario }} + node-version: ${{ env.NODE_VERSION }} + expected-failures: ./conformance-test/conformance-baseline.yml diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 0000000..e7e94c3 --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,23 @@ +name: Auto-merge Dependabot + +on: pull_request + +permissions: + contents: write + pull-requests: write + +jobs: + auto-merge: + if: github.actor == 'dependabot[bot]' + runs-on: ubuntu-latest + steps: + - uses: dependabot/fetch-metadata@v3 + id: metadata + + - if: >- + steps.metadata.outputs.package-ecosystem == 'github_actions' || + steps.metadata.outputs.package-ecosystem == 'npm_and_yarn' + run: gh pr merge --auto --squash "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/gradle-publish.yml b/.github/workflows/gradle-publish.yml index 53dde06..f01f9c8 100644 --- a/.github/workflows/gradle-publish.yml +++ b/.github/workflows/gradle-publish.yml @@ -1,40 +1,39 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. -# This workflow will build a package using Gradle and then publish it to GitHub packages when a release is created -# For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#Publishing-using-gradle - name: Release on: release: types: [ published ] + workflow_dispatch: jobs: build: - runs-on: macos-latest-xlarge + runs-on: macos-latest environment: release env: - JAVA_OPTS: "-Xmx8g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6g" + GRADLE_OPTS: "-Dorg.gradle.daemon=false" permissions: contents: write packages: write steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Set up JDK 21 - uses: actions/setup-java@v5 + uses: actions/setup-java@v6 with: java-version: '21' distribution: 'temurin' - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 + - name: Set up Node.js + uses: actions/setup-node@v7 + with: + node-version: '22' # increase only after https://github.com/nodejs/node/issues/56645 will be fixed - - name: Clean Build with Gradle - run: ./gradlew clean build + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6.3.0 + + - name: Check + run: ./gradlew check --no-configuration-cache - name: Publish to Maven Central Portal id: publish diff --git a/.github/workflows/samples.yml b/.github/workflows/samples.yml new file mode 100644 index 0000000..7463a9e --- /dev/null +++ b/.github/workflows/samples.yml @@ -0,0 +1,64 @@ +name: Build Samples + +on: + workflow_dispatch: + pull_request: + branches: [ main ] + paths: + - 'samples/**' + push: + branches: [ main ] + paths: + - 'samples/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + # Cancel only when the run is NOT on `main` branch + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +env: + JAVA_VERSION: 21 + JAVA_DISTRIBUTION: temurin + GRADLE_OPTS: "-Dorg.gradle.daemon=false" + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + sample: + - kotlin-mcp-client + - kotlin-mcp-server + - simple-streamable-server + - weather-stdio-server + + name: Build Sample (${{ matrix.sample }}) + timeout-minutes: 10 + steps: + - uses: actions/checkout@v7 + + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v6 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DISTRIBUTION }} + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6.3.0 + with: + validate-wrappers: true + add-job-summary: 'always' + cache-read-only: ${{ github.ref != 'refs/heads/main' }} + + - name: "Build Sample: ${{ matrix.sample }}" + working-directory: ./samples/${{ matrix.sample }} + run: ./gradlew build + + - name: Upload Reports + if: failure() + uses: actions/upload-artifact@v7 + with: + name: reports-samples-${{ matrix.sample }} + path: | + **/build/reports/ diff --git a/.gitignore b/.gitignore index c0631fa..805579c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,16 +11,19 @@ build/ .idea/compiler.xml .idea/libraries/ .idea -!.idea/icon.png *.iws *.iml *.ipr out/ !**/src/main/**/out/ !**/src/test/**/out/ +!.idea/icon.png +!.idea/detekt.xml +!.idea/externalDependencies.xml ### Kotlin ### .kotlin +kotlin-js-store yarn.lock ### Eclipse ### @@ -55,3 +58,6 @@ dist ### SWE agents ### .claude/ .junie/ + +### Conformance test results ### +conformance-test/results/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6ef38be --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,82 @@ +# AGENTS: Development Guidelines for AI Contributors + +This file provides guidance to AI coding agents working with code in this repository. + +## Project + +Kotlin Multiplatform SDK for the [Model Context Protocol (MCP)](https://modelcontextprotocol.io). Targets JVM, Native (macOS/Linux/Windows), JS, and Wasm. Implements both MCP clients and servers with pluggable transports. + +## Build Commands + +```bash +./gradlew ktlintCheck # Lint check +./gradlew detekt # Static analysis +./gradlew ktlintFormat # Auto-fix lint issues +./gradlew apiCheck # Check public API compatibility (run before committing) +./gradlew apiDump # Update API dump after intentional API changes +./gradlew koverLog # Print coverage summary +./gradlew koverXmlReport # Generate XML coverage report +``` + +Run a single test class: +```bash +./gradlew :kotlin-sdk-server:jvmTest --tests "io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransportTest" +``` + +Run a single test method: +```bash +./gradlew :kotlin-sdk-core:jvmTest --tests "*.ProtocolTest.should preserve existing meta when adding progress token" +``` + +Conformance tests (requires Node.js + npx): +```bash +./conformance-test/run-conformance.sh all # All suites sequentially +./conformance-test/run-conformance.sh server # Server suite only +./conformance-test/run-conformance.sh client # Client core suite (initialize, tools_call, elicitation, sse-retry) +./conformance-test/run-conformance.sh client-auth # Client auth suite (20 OAuth scenarios) +``` + +## Module Structure + +``` +kotlin-sdk-core Core: protocol types, JSON-RPC, Transport abstraction, Protocol base class +kotlin-sdk-client Client: MCP client implementation (tools, prompts, resources, sampling) +kotlin-sdk-server Server: MCP server implementation (feature registries, sessions) +kotlin-sdk-testing ChannelTransport for in-memory client-server testing +kotlin-sdk Umbrella module aggregating client + server +test-utils Shared test helpers (mockk, kotest, awaitility) +conformance-test MCP specification compliance tests (external runner) +integration-test End-to-end tests +samples/ Runnable examples (servers and clients) +``` + +## Architecture + +Layered design: + +``` +Application (tools, prompts, resources) + → Client / Server (typed MCP operations, capability enforcement) + → Protocol (JSON-RPC framing, request correlation, timeouts) + → Transport (pluggable I/O: stdio, SSE, Streamable HTTP, WebSocket) +``` + +## Code Conventions + +- **Explicit API mode (Strict)**: all public declarations must have explicit visibility modifiers and return types +- **DSL builders**: prefer DSL builder style (lambda blocks) over direct constructors +- **Multi-dollar interpolation**: use `$$"""..."""` for strings containing literal `$` (e.g., JSON with `$ref`, `$defs`) +- **`McpJson` has `explicitNulls = false`**: null properties must be absent from JSON, not `"field": null` +- **Logging**: use `io.github.oshai.kotlinlogging.KotlinLogging` +- **Structured concurrency**: every coroutine must have a bounded scope and lifetime; always re-throw `CancellationException`; never hold `Mutex` across suspension points + +## Testing + +- **JVM first**: run `jvmTest` instead of `test` — full multiplatform test runs are slow and rarely needed during development +- **Test naming**: use backtick syntax — `fun \`should return 200 OK\`()` +- **Frameworks**: JUnit 5 (runner) + Kotest (assertions) + MockK (mocking) + Awaitility (async waits) +- **In-memory testing**: `ChannelTransport.createLinkedPair()` connects client and server without network + +## Skills + +- `/mcp-docs` — fetches live MCP specification and docs; use when implementing or debugging MCP protocol features diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/LICENSE b/LICENSE index 136059a..185054b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,193 @@ +The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 ("Apache-2.0"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0. + +Contributions for which relicensing consent has been obtained are licensed under Apache-2.0. Contributions made by authors who originally licensed their work under the MIT License and who have not yet granted explicit permission to relicense remain licensed under the MIT License. + +No rights beyond those granted by the applicable original license are conveyed for such contributions. + +--- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright + owner or by an individual or Legal Entity authorized to submit on behalf + of the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + +--- + MIT License -Copyright (c) 2024 Anthropic, PBC +Copyright (c) 2024-2026 Model Context Protocol a Series of LF Projects, LLC. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +205,12 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. + +--- + +Creative Commons Attribution 4.0 International (CC-BY-4.0) + +Documentation in this project (excluding specifications) is licensed under +CC-BY-4.0. See https://creativecommons.org/licenses/by/4.0/legalcode for +the full license text. diff --git a/README.md b/README.md index 4fd0609..aefadcf 100644 --- a/README.md +++ b/README.md @@ -1,104 +1,318 @@ # MCP Kotlin SDK -[![Kotlin Multiplatform](https://img.shields.io/badge/Kotlin-Multiplatform-blueviolet?logo=kotlin)](https://kotlinlang.org/docs/multiplatform.html) -[![Platforms](https://img.shields.io/badge/Platforms-JVM%20%7C%20Wasm%2FJS%20%7C%20Native%20(iOS%2FiOS%20Simulator)-blue)](https://kotlinlang.org/docs/multiplatform.html) -[![Maven Central](https://img.shields.io/maven-central/v/io.modelcontextprotocol/kotlin-sdk.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:io.modelcontextprotocol%20a:kotlin-sdk) -[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) +[![Maven Central](https://img.shields.io/maven-central/v/io.modelcontextprotocol/kotlin-sdk.svg?label=Maven%20Central)](https://central.sonatype.com/artifact/io.modelcontextprotocol/kotlin-sdk) +[![Build](https://github.com/modelcontextprotocol/kotlin-sdk/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/modelcontextprotocol/kotlin-sdk/actions/workflows/build.yml) -Kotlin Multiplatform implementation of the [Model Context Protocol](https://modelcontextprotocol.io) (MCP), -providing both client and server capabilities for integrating with LLM surfaces across various platforms. +[![Kotlin](https://img.shields.io/badge/kotlin-2.2+-blueviolet.svg?logo=kotlin)](http://kotlinlang.org) +[![Kotlin Multiplatform](https://img.shields.io/badge/Platforms-%20JVM%20%7C%20Wasm%2FJS%20%7C%20Native%20-blueviolet?logo=kotlin)](https://kotlinlang.org/docs/multiplatform.html) +[![JVM](https://img.shields.io/badge/JVM-11+-red.svg?logo=jvm)](http://java.com) +[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE) + + +Kotlin Multiplatform SDK for the [Model Context Protocol](https://modelcontextprotocol.io). +It enables Kotlin applications targeting JVM, Native, JS, and Wasm to implement MCP clients and servers using a +standardized protocol interface. + +## Table of Contents + + + +* [Overview](#overview) +* [Installation](#installation) + * [Artifacts](#artifacts) + * [Gradle setup (JVM)](#gradle-setup-jvm) + * [Multiplatform](#multiplatform) + * [Ktor dependencies](#ktor-dependencies) +* [Quickstart](#quickstart) + * [Creating a Client](#creating-a-client) + * [Creating a Server](#creating-a-server) +* [Core Concepts](#core-concepts) + * [MCP Primitives](#mcp-primitives) + * [Capabilities](#capabilities) + * [Server Capabilities](#server-capabilities) + * [Client Capabilities](#client-capabilities) + * [Server Features](#server-features) + * [Prompts](#prompts) + * [Resources](#resources) + * [Tools](#tools) + * [Completion](#completion) + * [Logging](#logging) + * [Pagination](#pagination) + * [Client Features](#client-features) + * [Roots](#roots) + * [Sampling](#sampling) +* [Transports](#transports) + * [STDIO Transport](#stdio-transport) + * [Streamable HTTP Transport](#streamable-http-transport) + * [SSE Transport](#sse-transport) + * [WebSocket Transport](#websocket-transport) + * [ChannelTransport (testing)](#channeltransport-testing) +* [Connecting your server](#connecting-your-server) +* [Examples](#examples) +* [Documentation](#documentation) +* [Contributing](#contributing) +* [License](#license) + + ## Overview The Model Context Protocol allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. -This SDK implements the MCP specification for Kotlin, -enabling you to build applications that can communicate using MCP on the JVM, WebAssembly and iOS. +This Kotlin SDK implements the MCP specification, making it easy to: -- Build MCP clients that can connect to any MCP server -- Create MCP servers that expose resources, prompts and tools -- Use standard transports like stdio, SSE, and WebSocket -- Handle all MCP protocol messages and lifecycle events - -## Samples - -- [kotlin-mcp-server](./samples/kotlin-mcp-server): demonstrates a multiplatform (JVM, Wasm) MCP server setup with various features and transports. -- [weather-stdio-server](./samples/weather-stdio-server): shows how to build a Kotlin MCP server providing weather forecast and alerts using STDIO transport. -- [kotlin-mcp-client](./samples/kotlin-mcp-client): demonstrates building an interactive Kotlin MCP client that connects to an MCP server via STDIO and integrates with Anthropic’s API. +- Build MCP **clients** that can connect to any MCP server +- Create MCP **servers** that expose resources, prompts, and tools +- Target **JVM, Native, JS, and Wasm** from a single codebase +- Use standard transports like **stdio**, **SSE**, **Streamable HTTP**, and **WebSocket** +- Handle MCP protocol messages and lifecycle events with coroutine-friendly APIs ## Installation -Add the new repository to your build file: +### Artifacts + +- `io.modelcontextprotocol:kotlin-sdk` – umbrella SDK (client + server APIs) +- `io.modelcontextprotocol:kotlin-sdk-client` – client-only APIs +- `io.modelcontextprotocol:kotlin-sdk-server` – server-only APIs + +### Gradle setup (JVM) + +Add the Maven Central repository and the SDK dependency: ```kotlin repositories { mavenCentral() } -``` -Add the dependency: - -```kotlin dependencies { // See the badge above for the latest version implementation("io.modelcontextprotocol:kotlin-sdk:$mcpVersion") } ``` -MCP SDK uses [Ktor](https://ktor.io/), but does not come with a specific engine dependency. -You should add [Ktor client](https://ktor.io/docs/client-dependencies.html#engine-dependency) -and/or [Ktor server](https://ktor.io/docs/client-dependencies.html#engine-dependency) dependency -to your project yourself, e.g.: + +Use _kotlin-sdk-client_ or _kotlin-sdk-server_ if you only need one side of the API: + ```kotlin dependencies { - // for client: - implementation("io.ktor:ktor-client-cio:$ktorVersion") - // for server: - implementation("io.ktor:ktor-server-netty:$ktorVersion") - + implementation("io.modelcontextprotocol:kotlin-sdk-client:$mcpVersion") + implementation("io.modelcontextprotocol:kotlin-sdk-server:$mcpVersion") } ``` -## Quick Start +### Multiplatform + +In a Kotlin Multiplatform project you can add the SDK to commonMain: + +```kotlin +commonMain { + dependencies { + // Works as a common dependency as well as the platform one + implementation("io.modelcontextprotocol:kotlin-sdk:$mcpVersion") + } +} +``` + +### Ktor dependencies + +The Kotlin MCP SDK uses [Ktor](https://ktor.io/), but it does not add Ktor engine dependencies transitively. +You need to declare +Ktor [client](https://ktor.io/docs/client-dependencies.html#engine-dependency)/[server](https://ktor.io/docs/server-dependencies.html) +dependencies yourself (or reuse the ones already used in your project), +for example: + +```kotlin +dependencies { + // MCP client with Ktor + implementation("io.ktor:ktor-client-cio:$ktorVersion") + implementation("io.modelcontextprotocol:kotlin-sdk-client:$mcpVersion") + + // MCP server with Ktor + implementation("io.ktor:ktor-server-netty:$ktorVersion") + implementation("io.modelcontextprotocol:kotlin-sdk-server:$mcpVersion") +} +``` + +## Quickstart + +Let's create a simple MCP client and server to demonstrate the basic usage of the Kotlin SDK. + + ### Creating a Client +Create an MCP client that connects to a server via Streamable HTTP transport and lists available tools: + ```kotlin +import io.ktor.client.HttpClient +import io.ktor.client.plugins.sse.SSE import io.modelcontextprotocol.kotlin.sdk.client.Client -import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport -import io.modelcontextprotocol.kotlin.sdk.Implementation +import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import kotlinx.coroutines.runBlocking -val client = Client( - clientInfo = Implementation( - name = "example-client", - version = "1.0.0" +fun main(args: Array) = runBlocking { + val url = args.firstOrNull() ?: "http://localhost:3000/mcp" + + val httpClient = HttpClient { install(SSE) } + + val client = Client( + clientInfo = Implementation( + name = "example-client", + version = "1.0.0" + ) ) -) -val transport = StdioClientTransport( - inputStream = processInputStream, - outputStream = processOutputStream -) + val transport = StreamableHttpClientTransport( + client = httpClient, + url = url + ) -// Connect to server -client.connect(transport) + // Connect to server + client.connect(transport) -// List available resources -val resources = client.listResources() + // List available tools + val tools = client.listTools().tools -// Read a specific resource -val resourceContent = client.readResource( - ReadResourceRequest(uri = "file:///example.txt") -) + println(tools) +} ``` + + ### Creating a Server +Create an MCP server that exposes a simple tool and runs on an embedded Ktor server with Streamable HTTP transport. +For a full working project with all required dependencies, see +the [simple-streamable-server](samples/simple-streamable-server) sample. + + + ```kotlin +import io.ktor.server.cio.CIO +import io.ktor.server.engine.embeddedServer import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions -import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport -import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.server.mcpStreamableHttp +import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +fun main(args: Array) { + val port = args.firstOrNull()?.toIntOrNull() ?: 3000 + val mcpServer = Server( + serverInfo = Implementation( + name = "example-server", + version = "1.0.0" + ), + options = ServerOptions( + capabilities = ServerCapabilities( + tools = ServerCapabilities.Tools(listChanged = true), + ), + ) + ) + + mcpServer.addTool( + name = "example-tool", + description = "An example tool", + inputSchema = ToolSchema( + properties = buildJsonObject { + put("input", buildJsonObject { put("type", "string") }) + } + ) + ) { request -> + CallToolResult(content = listOf(TextContent("Hello, world!"))) + } + + embeddedServer(CIO, host = "127.0.0.1", port = port) { + mcpStreamableHttp { + mcpServer + } + }.start(wait = true) +} +``` + + + +You can run the server and then connect to it using the client or test with the MCP Inspector: + +```bash +npx -y @modelcontextprotocol/inspector +``` + +In the inspector UI, connect to `http://localhost:3000/mcp`. + +## Core Concepts + +### MCP Primitives + +The MCP protocol defines core primitives that enable communication between servers and clients: + +| Primitive | Server Role | Client Role | Description | +|---------------|---------------------------------------------------|----------------------------------------|---------------------------------------------------| +| **Prompts** | Provides prompt templates with optional arguments | Requests and uses prompts | Interactive templates for LLM interactions | +| **Resources** | Exposes data sources (files, API responses, etc.) | Reads and subscribes to resources | Contextual data for augmenting LLM context | +| **Tools** | Defines executable functions | Calls tools to perform actions | Functions the LLM can invoke to take actions | +| **Sampling** | Requests LLM completions from client | Executes LLM calls and returns results | Server-initiated LLM requests (reverse direction) | + +### Capabilities + +Capabilities define what features a server or client supports. They are declared during initialization and determine +what operations are available. + +#### Server Capabilities + +Servers declare their capabilities to inform clients what features they provide: + +| Capability | Feature Flags | Description | +|----------------|-------------------------------|------------------------------------------------------------| +| `prompts` | `listChanged` | Prompt template management and notifications | +| `resources` | `subscribe`
`listChanged` | Resource exposure, subscriptions, and update notifications | +| `tools` | `listChanged` | Tool discovery, execution, and list change notifications | +| `logging` | - | Server logging to client console | +| `completions` | - | Argument autocompletion suggestions | +| `experimental` | Custom properties | Non-standard experimental features | + +#### Client Capabilities + +Clients declare their capabilities to inform servers what features they support: + +| Capability | Feature Flags | Description | +|----------------|-------------------|-------------------------------------------------------------| +| `sampling` | - | Client can sample from an LLM (execute model requests) | +| `roots` | `listChanged` | Client exposes root directories and can notify of changes | +| `elicitation` | - | Client can display schema/form dialogs for structured input | +| `experimental` | Custom properties | Non-standard experimental features | + +### Server Features + +The `Server` API lets you wire prompts, resources, and tools with only a few lines of Kotlin. Each feature is registered +up front and then resolved lazily when a client asks for it, so your handlers stay small and suspendable. + +#### Prompts + +Prompts are user-controlled templates that clients discover via `prompts/list` and fetch with `prompts/get` when a user +chooses one (think slash commands or saved flows). They’re best for repeatable, structured starters rather than ad-hoc +model calls. + + + +```kotlin val server = Server( serverInfo = Implementation( name = "example-server", @@ -106,95 +320,607 @@ val server = Server( ), options = ServerOptions( capabilities = ServerCapabilities( - resources = ServerCapabilities.Resources( - subscribe = true, - listChanged = true - ) - ) + prompts = ServerCapabilities.Prompts(listChanged = true), + ), + ) +) + +server.addPrompt( + name = "code-review", + description = "Ask the model to review a diff", + arguments = listOf( + PromptArgument(name = "diff", description = "Unified diff", required = true), + ), +) { request -> + GetPromptResult( + description = "Quick code review helper", + messages = listOf( + PromptMessage( + role = Role.User, + content = TextContent(text = "Review this change:\n${request.arguments?.get("diff")}"), + ), + ), + ) +} +``` + + + + + +Use prompts for anything that deserves a template: bug triage questions, onboarding checklists, or saved searches. Set +`listChanged = true` only if your prompt catalog can change at runtime and your server will emit +`notifications/prompts/list_changed` when it does. + +#### Resources + +Resources are application-driven context that clients discover via `resources/list` or `resources/templates/list`, then +fetch with `resources/read`. Register each one with a stable URI and return a `ReadResourceResult` when asked—contents +can be text or binary blobs. + + + +```kotlin +val server = Server( + serverInfo = Implementation( + name = "example-server", + version = "1.0.0" + ), + options = ServerOptions( + capabilities = ServerCapabilities( + resources = ServerCapabilities.Resources(subscribe = true, listChanged = true), + ), ) ) -// Add a resource server.addResource( - uri = "file:///example.txt", - name = "Example Resource", - description = "An example text file", - mimeType = "text/plain" + uri = "note://release/latest", + name = "Release notes", + description = "Last deployment summary", + mimeType = "text/markdown", ) { request -> ReadResourceResult( contents = listOf( TextResourceContents( - text = "This is the content of the example resource.", + text = "Ship 42 reached production successfully.", uri = request.uri, - mimeType = "text/plain" - ) - ) + mimeType = "text/markdown", + ), + ), ) } - -// Start server with stdio transport -val transport = StdioServerTransport() -server.connect(transport) ``` -### Using SSE Transport + + + + +Resources can be static text, generated JSON, or blobs—anything the client can surface to the user or inject into the +model context. Set `subscribe = true` if you emit `notifications/resources/updated` for changes to specific URIs, and +`listChanged = true` if you’ll send `notifications/resources/list_changed` when the catalog itself changes. + +#### Tools + +Tools are model-controlled capabilities the client exposes to the model. Clients discover them via `tools/list`, invoke +them with `tools/call`, and your handlers receive JSON arguments, can emit streaming logs or progress, and return a +`CallToolResult`. Keep a human in the loop for sensitive operations. + + -Directly in Ktor's `Application`: ```kotlin -import io.ktor.server.application.* -import io.modelcontextprotocol.kotlin.sdk.server.mcp +val server = Server( + serverInfo = Implementation( + name = "example-server", + version = "1.0.0" + ), + options = ServerOptions( + capabilities = ServerCapabilities( + tools = ServerCapabilities.Tools(listChanged = true), + ), + ) +) -fun Application.module() { - mcp { - Server( - serverInfo = Implementation( - name = "example-sse-server", - version = "1.0.0" - ), - options = ServerOptions( - capabilities = ServerCapabilities( - prompts = ServerCapabilities.Prompts(listChanged = null), - resources = ServerCapabilities.Resources(subscribe = null, listChanged = null) - ) - ) - ) - } +server.addTool( + name = "echo", + description = "Return whatever the user sent back to them", +) { request -> + val text = request.arguments?.get("text")?.jsonPrimitive?.content ?: "(empty)" + CallToolResult(content = listOf(TextContent(text = "Echo: $text"))) } ``` -Inside a custom Ktor's `Route`: + + + + +Register as many tools as you need—long-running jobs can report progress via the request context, and tools can also +trigger sampling (see below) when they need the client’s LLM. Set `listChanged = true` only if your tool catalog can +change at runtime and your server will emit `notifications/tools/list_changed` when it does. + +#### Completion + +Completion provides argument suggestions for prompts or resource templates. Declare the `completions` capability and +handle `completion/complete` requests to return up to 100 ranked values (include `total`/`hasMore` if you paginate). + + + ```kotlin -import io.ktor.server.application.* +val server = Server( + serverInfo = Implementation( + name = "example-server", + version = "1.0.0" + ), + options = ServerOptions( + capabilities = ServerCapabilities( + completions = ServerCapabilities.Completions, + ), + ) +) + +val session = server.createSession( + StdioServerTransport( + inputStream = System.`in`.asSource().buffered(), + outputStream = System.out.asSink().buffered() + ) +) + +session.setRequestHandler(Method.Defined.CompletionComplete) { request, _ -> + val options = listOf("kotlin", "compose", "coroutine") + val matches = options.filter { it.startsWith(request.argument.value.lowercase()) } + + CompleteResult( + completion = CompleteResult.Completion( + values = matches.take(3), + total = matches.size, + hasMore = matches.size > 3, + ), + ) +} +``` + + + + + +Use `context.arguments` to refine suggestions for dependent fields (e.g., framework list filtered by chosen language). + +#### Logging + +Logging lets the server stream structured log notifications to the client using RFC 5424 levels (`debug` → `emergency`). +Declare the `logging` capability; clients can raise the minimum level with `logging/setLevel`, and the server emits +`notifications/message` with severity, optional logger name, and JSON data. + + + +```kotlin +val server = Server( + serverInfo = Implementation("example-server", "1.0.0"), + options = ServerOptions( + capabilities = ServerCapabilities( + logging = ServerCapabilities.Logging, + ), + ) +) + +val session = server.createSession( + StdioServerTransport( + inputStream = System.`in`.asSource().buffered(), + outputStream = System.out.asSink().buffered() + ) +) + +session.sendLoggingMessage( + LoggingMessageNotification( + LoggingMessageNotificationParams( + level = LoggingLevel.Info, + logger = "startup", + data = buildJsonObject { put("message", "Server started") }, + ), + ), +) +``` + + + + + +Keep logs free of sensitive data, and expect clients to surface them in their own UI. + +#### Pagination + +List operations return paginated results with an opaque `nextCursor`, clients echo that cursor to fetch the next page. +Supported list calls: `resources/list`, `resources/templates/list`, `prompts/list`, and `tools/list`. +Treat cursors as opaque—don’t parse or persist them across sessions. + + + +```kotlin +val server = Server( + serverInfo = Implementation("example-server", "1.0.0"), + options = ServerOptions( + capabilities = ServerCapabilities( + resources = ServerCapabilities.Resources(), + ), + ) +) + +val session = server.createSession( + StdioServerTransport( + inputStream = System.`in`.asSource().buffered(), + outputStream = System.out.asSink().buffered() + ) +) + +val resources = listOf( + Resource(uri = "note://1", name = "Note 1", description = "First"), + Resource(uri = "note://2", name = "Note 2", description = "Second"), + Resource(uri = "note://3", name = "Note 3", description = "Third"), +) +val pageSize = 2 + +session.setRequestHandler(Method.Defined.ResourcesList) { request, _ -> + val start = request.params?.cursor?.toIntOrNull() ?: 0 + val page = resources.drop(start).take(pageSize) + val next = if (start + page.size < resources.size) (start + page.size).toString() else null + + ListResourcesResult( + resources = page, + nextCursor = next, + ) +} +``` + + + + + +Include `nextCursor` only when more items remain an absent cursor ends pagination. + +### Client Features + +Clients advertise their capabilities (roots, sampling, elicitation, etc.) during initialization. After that they can +serve requests from the server while still initiating calls such as `listTools` or `callTool`. + +#### Roots + +Roots let the client declare where the server is allowed to operate. Declare the `roots` capability, respond to +`roots/list`, and emit `notifications/roots/list_changed` if you set `listChanged = true`. URIs **must** be `file://` +paths. + + + +```kotlin +val client = Client( + clientInfo = Implementation("demo-client", "1.0.0"), + options = ClientOptions( + capabilities = ClientCapabilities(roots = ClientCapabilities.Roots(listChanged = true)), + ), +) + +client.addRoot( + uri = "file:///Users/demo/projects", + name = "Projects", +) +client.sendRootsListChanged() +``` + + + + + +Call `addRoot`/`removeRoot` whenever your file system view changes, and use `sendRootsListChanged()` to notify the +server. Keep root lists user-controlled and revoke entries that are no longer authorized. + +#### Sampling + +Sampling lets the server ask the client to call its preferred LLM. Declare the `sampling` capability (and +`sampling.tools` if you allow tool-enabled sampling), and handle `sampling/createMessage`. Keep a human in the loop for +approvals. + + + +```kotlin +val client = Client( + clientInfo = Implementation("demo-client", "1.0.0"), + options = ClientOptions( + capabilities = ClientCapabilities( + sampling = buildJsonObject { putJsonObject("tools") { } }, // drop tools if you don't support tool use + ), + ), +) + +client.setRequestHandler(Method.Defined.SamplingCreateMessage) { request, _ -> + val content = request.messages.lastOrNull()?.content?.firstOrNull() + val prompt = if (content is TextContent) content.text else "your topic" + CreateMessageResult( + model = "gpt-4o-mini", + role = Role.Assistant, + content = TextContent(text = "Here is a short note about $prompt"), + ) +} +``` + + + + + +Inside the handler you can pick any model/provider, require approvals, or reject the request. If you don’t support tool +use, omit `sampling.tools` from capabilities. + +[//]: # (TODO: add elicitation section) + +[//]: # (#### Elicitation) + +## Transports + +All transports share the same API surface, so you can change deployment style without touching business logic. Pick the +transport that best matches where the server runs. + +### STDIO Transport + +`StdioClientTransport` and `StdioServerTransport` tunnel MCP messages over stdin/stdout—perfect for editor plugins or +CLI tooling that spawns a helper process. No networking setup is required. + +### Streamable HTTP Transport + +`StreamableHttpClientTransport` and the Ktor `mcpStreamableHttp()` / `mcpStatelessStreamableHttp()` helpers expose MCP +over a single HTTP endpoint with optional JSON-only or SSE streaming responses. This is the recommended choice for +remote deployments and integrates nicely with proxies or service meshes. + +These helpers automatically install `ContentNegotiation` with `McpJson` — do not install it yourself, or a warning will +be logged. Both accept a `path` parameter (default: `"/mcp"`) to mount the endpoint at any URL: + + + + +```kotlin +embeddedServer(CIO, port = 3000) { + mcpStreamableHttp(path = "/api/mcp") { + MyServer() + } +}.start(wait = true) +``` + + + + +**CORS for browser-based clients (e.g. MCP Inspector):** if you connect from a browser-based +client you need to install the Ktor CORS plugin so that MCP-specific headers are allowed and exposed: + +```kotlin +install(CORS) { + anyHost() // restrict to specific origins in production + allowMethod(HttpMethod.Options) + allowMethod(HttpMethod.Get) + allowMethod(HttpMethod.Post) + allowMethod(HttpMethod.Delete) + allowNonSimpleContentTypes = true + allowHeader("Mcp-Session-Id") + allowHeader("Mcp-Protocol-Version") + exposeHeader("Mcp-Session-Id") + exposeHeader("Mcp-Protocol-Version") +} +``` + +### SSE Transport + +Server-Sent Events remain available for backwards compatibility with older MCP clients. Two Ktor helpers are provided: + +- **`Application.mcp { }`** — installs SSE and `ContentNegotiation` with `McpJson` automatically, then registers MCP + endpoints at `/`. Do not install `ContentNegotiation` yourself — the SDK handles it. +- **`Route.mcp { }`** — registers MCP endpoints at the current route path; requires `install(SSE)` in the application + first. Use this to host MCP alongside other routes or under a path prefix: + + + + +```kotlin +embeddedServer(CIO, port = 3000) { install(SSE) - routing { - route("myRoute") { - mcp { - Server( - serverInfo = Implementation( - name = "example-sse-server", - version = "1.0.0" - ), - options = ServerOptions( - capabilities = ServerCapabilities( - prompts = ServerCapabilities.Prompts(listChanged = null), - resources = ServerCapabilities.Resources(subscribe = null, listChanged = null) - ) - ) - ) - } + route("/api/mcp") { + mcp { MyServer() } } } -} +}.start(wait = true) ``` + + + + +Prefer Streamable HTTP for new projects. + +### WebSocket Transport + +`WebSocketClientTransport` plus the matching server utilities provide full-duplex, low-latency connections—useful when +you expect lots of notifications or long-running sessions behind a reverse proxy that already terminates WebSockets. + +### ChannelTransport (testing) + +`ChannelTransport` provides a simple, non-networked transport for testing and local development. +It uses Kotlin coroutines channels to provide a full-duplex connection between a client and server, +allowing for easy testing of MCP functionality without the need for network setup. + +## Connecting your server + +1. Start a sample HTTP server on port 3000: + + ```bash + ./gradlew :samples:kotlin-mcp-server:run + ``` + +2. Connect with the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) or Claude Desktop/Code: + + ```bash + npx -y @modelcontextprotocol/inspector --connect http://localhost:3000 + # or + claude mcp add --transport http kotlin-mcp http://localhost:3000 + ``` + +3. In the Inspector, confirm prompts, tools, resources, completions, and logs show up. Iterate locally until you’re + ready to host the server wherever you prefer. + +## Examples + +The [samples](./samples) directory contains runnable projects demonstrating +MCP server and client implementations with various transports. +See the [samples overview](./samples/README.md) for a comparison table and detailed descriptions. + +## Documentation +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/modelcontextprotocol/kotlin-sdk) + +- [API Reference](https://kotlin.sdk.modelcontextprotocol.io/) +- [Model Context Protocol documentation](https://modelcontextprotocol.io) +- [MCP specification](https://modelcontextprotocol.io/specification/latest) + ## Contributing Please see the [contribution guide](CONTRIBUTING.md) and the [Code of conduct](CODE_OF_CONDUCT.md) before contributing. ## License -This project is licensed under the MIT License—see the [LICENSE](LICENSE) file for details. +This project is licensed under Apache 2.0 for new contributions, with existing code under MIT—see the [LICENSE](LICENSE) +file for details. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..5029242 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +Thank you for helping keep the Model Context Protocol and its ecosystem secure. + +## Reporting Security Issues + +If you discover a security vulnerability in this repository, please report it through +the [GitHub Security Advisory process](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability) +for this repository. + +Please **do not** report security vulnerabilities through public GitHub issues, discussions, +or pull requests. + +## What to Include + +To help us triage and respond quickly, please include: + +- A description of the vulnerability +- Steps to reproduce the issue +- The potential impact +- Any suggested fixes (optional) diff --git a/build.gradle.kts b/build.gradle.kts index 67cb5d9..2c2dbe3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,20 +4,17 @@ plugins { alias(libs.plugins.kover) } -allprojects { - group = "io.modelcontextprotocol" - version = "0.7.2-alpha3" -} - dependencies { dokka(project(":kotlin-sdk-core")) dokka(project(":kotlin-sdk-client")) dokka(project(":kotlin-sdk-server")) + dokka(project(":kotlin-sdk-testing")) kover(project(":kotlin-sdk-core")) kover(project(":kotlin-sdk-client")) kover(project(":kotlin-sdk-server")) - kover(project(":kotlin-sdk-test")) + kover(project(":kotlin-sdk-testing")) + kover(project(":integration-test")) } subprojects { @@ -33,17 +30,28 @@ dokka { } } +ktlint { + filter { + exclude("**/generated*/**") + } +} + kover { reports { filters { includes.classes("io.modelcontextprotocol.kotlin.sdk.*") + excludes { + annotatedBy("kotlin.Deprecated") + classes("io.modelcontextprotocol.kotlin.sdk.models.*") // temporary + classes("io.modelcontextprotocol.kotlin.sdk.models.infrastructure.*") // generated + } } total { log { } verify { rule { - minBound(65) + minBound(73) } } } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 84676a4..4a057ce 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -8,6 +8,7 @@ repositories { } dependencies { + implementation(libs.detekt.gradle) implementation(libs.kotlin.gradle) implementation(libs.kotlin.serialization) implementation(libs.kotlinx.atomicfu.gradle) diff --git a/buildSrc/src/main/kotlin/mcp.detekt.gradle.kts b/buildSrc/src/main/kotlin/mcp.detekt.gradle.kts new file mode 100644 index 0000000..3c1419e --- /dev/null +++ b/buildSrc/src/main/kotlin/mcp.detekt.gradle.kts @@ -0,0 +1,23 @@ +import dev.detekt.gradle.Detekt +import dev.detekt.gradle.extensions.FailOnSeverity + +plugins { + id("dev.detekt") +} + +detekt { + config = files("$rootDir/config/detekt/detekt.yml") + buildUponDefaultConfig = false + parallel = true + failOnSeverity = FailOnSeverity.Error +} + +tasks.withType().configureEach { + exclude("**/generated/**", "**/generated-sources/**", "**/build/**") +} + +pluginManager.withPlugin("org.jetbrains.kotlin.multiplatform") { + tasks.named("detekt").configure { + dependsOn("detektMainJvm") + } +} diff --git a/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts b/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts index e115716..98a0a5e 100644 --- a/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts +++ b/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts @@ -1,8 +1,11 @@ @file:OptIn(ExperimentalWasmDsl::class) +import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode +import org.jetbrains.kotlin.gradle.dsl.JvmDefaultMode import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion plugins { kotlin("multiplatform") @@ -11,11 +14,42 @@ plugins { } kotlin { - jvm { - compilerOptions.jvmTarget = JvmTarget.JVM_1_8 + + compilerOptions { + languageVersion = KotlinVersion.KOTLIN_2_1 + apiVersion = KotlinVersion.KOTLIN_2_1 + freeCompilerArgs = + listOf( + "-Wextra", + "-Xmulti-dollar-interpolation", + ) } - macosX64(); macosArm64() - linuxX64(); linuxArm64() + jvm { + compilerOptions { + jvmTarget = JvmTarget.JVM_11 + javaParameters = true + jvmDefault = JvmDefaultMode.ENABLE + freeCompilerArgs.addAll( + "-Xdebug", + ) + } + + tasks.withType().configureEach { + + useJUnitPlatform() + + maxParallelForks = Runtime.getRuntime().availableProcessors() + forkEvery = 100 + testLogging { + exceptionFormat = TestExceptionFormat.SHORT + events("failed") + } + systemProperty("kotest.output.ansi", "true") + } + } + macosArm64() + linuxX64() + linuxArm64() mingwX64() js { nodejs() } wasmJs { nodejs() } diff --git a/buildSrc/src/main/kotlin/netty-convention.gradle.kts b/buildSrc/src/main/kotlin/netty-convention.gradle.kts new file mode 100644 index 0000000..920baf0 --- /dev/null +++ b/buildSrc/src/main/kotlin/netty-convention.gradle.kts @@ -0,0 +1,61 @@ +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension + +/** + * Netty convention plugin that adds platform-specific Netty native transport libraries + * to the jvmTest source set. This is similar to how Maven handles OS-specific dependencies + * with profile activation. + * + * This plugin should be applied to any module that uses Netty for testing. + */ +val nettyVersion = "4.2.7.Final" +plugins { + id("org.gradle.base") +} + +// This plugin is applied to projects that already have the kotlin multiplatform plugin applied +// It adds the Netty native transport libraries to the jvmTest source set + +afterEvaluate { + + extensions.findByType()?.apply { + sourceSets.findByName("jvmTest")?.apply { + dependencies { + // Netty native transport libraries for different platforms + val osName = System.getProperty("os.name").lowercase() + val osArch = System.getProperty("os.arch").lowercase() + + // Add the base Netty platform + implementation(project.dependencies.platform("io.netty:netty-bom:$nettyVersion")) + + when { + osName.contains("linux") -> { + val archClassifier = + if (osArch.contains("aarch64")) { + "linux-aarch_64" + } else { + "linux-x86_64" + } + runtimeOnly( + "io.netty:netty-transport-native-epoll:$nettyVersion:$archClassifier", + ) + } + + osName.contains("mac") -> { + val archClassifier = + if (osArch.contains("aarch64")) { + "osx-aarch_64" + } else { + "osx-x86_64" + } + runtimeOnly( + "io.netty:netty-transport-native-kqueue:$nettyVersion:$archClassifier", + ) + runtimeOnly( + "io.netty:netty-resolver-dns-native-macos:$nettyVersion:$archClassifier", + ) + } + } + } + } + } +} diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml new file mode 100644 index 0000000..a82b404 --- /dev/null +++ b/config/detekt/detekt.yml @@ -0,0 +1,841 @@ +config: + validation: true + warningsAsErrors: false + checkExhaustiveness: false + +processors: + active: false + +console-reports: + active: true + exclude: + - 'ProjectStatisticsReport' + - 'ComplexityReport' + - 'NotificationReport' + - 'IssuesReport' + - 'FileBasedIssuesReport' + # - 'LiteIssuesReport' + +comments: + active: true + AbsentOrWrongFileLicense: + active: false + licenseTemplateIsRegex: false + licenseTemplate: '' + DeprecatedBlockTag: + active: true + DocumentationOverPrivateFunction: + active: false + DocumentationOverPrivateProperty: + active: false + EndOfSentenceFormat: + active: false + endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' + KDocReferencesNonPublicProperty: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + OutdatedDocumentation: + active: false + matchTypeParameters: true + matchDeclarationsOrder: true + allowParamOnConstructorProperties: false + UndocumentedPublicClass: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchInNestedClass: true + searchInInnerClass: true + searchInInnerObject: true + searchInInnerInterface: true + searchInProtectedClass: false + ignoreDefaultCompanionObject: true + UndocumentedPublicFunction: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchProtectedFunction: false + UndocumentedPublicProperty: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchProtectedProperty: false + ignoreEnumEntries: false + +complexity: + active: true + CognitiveComplexMethod: + active: false + allowedComplexity: 20 + ComplexCondition: + active: true + allowedConditions: 4 + ComplexInterface: + active: false + allowedDefinitions: 10 + includeStaticDeclarations: false + includePrivateDeclarations: false + ignoreOverloaded: false + CyclomaticComplexMethod: + active: false + allowedComplexity: 15 + ignoreSingleWhenExpression: false + ignoreSimpleWhenEntries: false + ignoreNestingFunctions: false + ignoreLocalFunctions: false + nestingFunctions: + - 'also' + - 'apply' + - 'forEach' + - 'isNotNull' + - 'ifNull' + - 'let' + - 'run' + - 'use' + - 'with' + LabeledExpression: + active: false + ignoredLabels: [] + LargeClass: + active: false + allowedLines: 700 + LongMethod: + active: false + allowedLines: 110 + LongParameterList: + active: false + allowedFunctionParameters: 8 + allowedConstructorParameters: 8 + ignoreDefaultParameters: false + ignoreDataClasses: true + ignoreAnnotatedParameter: [] + MethodOverloading: + active: false + allowedOverloads: 6 + NamedArguments: + active: false + allowedArguments: 3 + ignoreMethods: [] + ignoreArgumentsMatchingNames: false + NestedBlockDepth: + active: true + allowedDepth: 4 + NestedScopeFunctions: + active: false + allowedDepth: 1 + functions: + - 'kotlin.apply' + - 'kotlin.run' + - 'kotlin.with' + - 'kotlin.let' + - 'kotlin.also' + ReplaceSafeCallChainWithRun: + active: false + StringLiteralDuplication: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + allowedDuplications: 3 + ignoreAnnotation: true + allowedWithLengthLessThan: 5 + ignoreStringsRegex: '$^' + TooManyFunctions: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + allowedFunctionsPerFile: 20 + allowedFunctionsPerClass: 20 + allowedFunctionsPerInterface: 20 + allowedFunctionsPerObject: 20 + allowedFunctionsPerEnum: 11 + ignoreDeprecated: true + ignorePrivate: true + ignoreInternal: true + ignoreOverridden: false + ignoreAnnotatedFunctions: [] + +coroutines: + active: true + CoroutineLaunchedInTestWithoutRunTest: + active: false + GlobalCoroutineUsage: + active: true + InjectDispatcher: + active: false + dispatcherNames: + - 'IO' + - 'Default' + - 'Unconfined' + RedundantSuspendModifier: + active: true + SleepInsteadOfDelay: + active: true + SuspendFunInFinallySection: + active: false + SuspendFunSwallowedCancellation: + active: false + SuspendFunWithCoroutineScopeReceiver: + active: false + aliases: ['SuspendFunctionOnCoroutineScope'] + SuspendFunWithFlowReturnType: + active: true + +empty-blocks: + active: true + EmptyCatchBlock: + active: true + allowedExceptionNameRegex: '_|(ignore|expected).*' + EmptyClassBlock: + active: true + EmptyDefaultConstructor: + active: true + EmptyDoWhileBlock: + active: true + EmptyElseBlock: + active: true + EmptyFinallyBlock: + active: true + EmptyForBlock: + active: true + EmptyFunctionBlock: + active: true + ignoreOverridden: false + EmptyIfBlock: + active: true + EmptyInitBlock: + active: true + EmptyKotlinFile: + active: true + EmptySecondaryConstructor: + active: true + EmptyTryBlock: + active: true + EmptyWhenBlock: + active: true + EmptyWhileBlock: + active: true + +exceptions: + active: true + ErrorUsageWithThrowable: + active: false + ExceptionRaisedInUnexpectedLocation: + active: true + methodNames: + - 'equals' + - 'finalize' + - 'hashCode' + - 'toString' + InstanceOfCheckForException: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + NotImplementedDeclaration: + active: true + ObjectExtendsThrowable: + active: true + PrintStackTrace: + active: true + RethrowCaughtException: + active: true + ReturnFromFinally: + active: true + ignoreLabeled: false + SwallowedException: + active: true + ignoredExceptionTypes: + - 'InterruptedException' + - 'MalformedURLException' + - 'NumberFormatException' + - 'ParseException' + allowedExceptionNameRegex: '_|(ignore|expected).*' + ThrowingExceptionFromFinally: + active: true + ThrowingExceptionInMain: + active: false + ThrowingExceptionsWithoutMessageOrCause: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptions: + - 'ArrayIndexOutOfBoundsException' + - 'Exception' + - 'IllegalArgumentException' + - 'IllegalMonitorStateException' + - 'IllegalStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + ThrowingNewInstanceOfSameException: + active: true + TooGenericExceptionCaught: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptionNames: + - 'ArrayIndexOutOfBoundsException' + - 'Error' + - 'Exception' + - 'IllegalMonitorStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + allowedExceptionNameRegex: '_|(ignore|expected).*' + TooGenericExceptionThrown: + active: false + exceptionNames: + - 'Error' + - 'Exception' + - 'RuntimeException' + - 'Throwable' + +naming: + active: true + BooleanPropertyNaming: + active: false + allowedPattern: '^(is|has|are)' + ClassNaming: + active: true + aliases: ['ClassName'] + classPattern: '[A-Z][a-zA-Z0-9]*' + ConstructorParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + privateParameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + EnumNaming: + active: true + aliases: ['EnumEntryName'] + enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' + ForbiddenClassName: + active: false + forbiddenName: [] + FunctionNameMaxLength: + active: false + aliases: ['FunctionMaxNameLength'] + maximumFunctionNameLength: 30 + FunctionNameMinLength: + active: false + aliases: ['FunctionMinNameLength'] + minimumFunctionNameLength: 3 + FunctionNaming: + active: true + aliases: ['FunctionName'] + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + functionPattern: '[a-z][a-zA-Z0-9]*' + excludeClassPattern: '$^' + FunctionParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + InvalidPackageDeclaration: + active: true + aliases: ['PackageDirectoryMismatch'] + rootPackage: '' + requireRootInDeclaration: false + LambdaParameterNaming: + active: false + parameterPattern: '[a-z][A-Za-z0-9]*|_' + MatchingDeclarationName: + active: false + mustBeFirst: true + multiplatformTargets: + - 'ios' + - 'android' + - 'js' + - 'jvm' + - 'native' + - 'iosArm64' + - 'iosX64' + - 'macosX64' + - 'mingwX64' + - 'linuxX64' + MemberNameEqualsClassName: + active: true + ignoreOverridden: true + NoNameShadowing: + active: true + NonBooleanPropertyPrefixedWithIs: + active: true + ObjectPropertyNaming: + active: true + aliases: ['ObjectPropertyName'] + constantPattern: '[A-Za-z][_A-Za-z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' + PackageNaming: + active: true + aliases: ['PackageName'] + packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' + TopLevelPropertyNaming: + active: true + constantPattern: '[A-Z][_A-Z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' + VariableMaxLength: + active: false + maximumVariableNameLength: 64 + VariableMinLength: + active: false + minimumVariableNameLength: 1 + VariableNaming: + active: false + aliases: ['PropertyName'] + variablePattern: '[a-z][A-Za-z0-9]*' + privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + +performance: + active: true + ArrayPrimitive: + active: true + CouldBeSequence: + active: false + allowedOperations: 3 + ForEachOnRange: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + SpreadOperator: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + UnnecessaryInitOnArray: + active: false + UnnecessaryPartOfBinaryExpression: + active: true + UnnecessaryTemporaryInstantiation: + active: true + UnnecessaryTypeCasting: + active: true + +potential-bugs: + active: true + AvoidReferentialEquality: + active: true + forbiddenTypePatterns: + - 'kotlin.String' + CastNullableToNonNullableType: + active: true + ignorePlatformTypes: true + CastToNullableType: + active: false + CharArrayToStringCall: + active: false + Deprecation: + active: false + aliases: ['DEPRECATION'] + DontDowncastCollectionTypes: + active: false + DoubleMutabilityForCollection: + active: true + aliases: ['DoubleMutability'] + mutableTypes: + - 'kotlin.collections.MutableList' + - 'kotlin.collections.MutableMap' + - 'kotlin.collections.MutableSet' + - 'java.util.ArrayList' + - 'java.util.LinkedHashSet' + - 'java.util.HashSet' + - 'java.util.LinkedHashMap' + - 'java.util.HashMap' + ElseCaseInsteadOfExhaustiveWhen: + active: false + ignoredSubjectTypes: [] + EqualsAlwaysReturnsTrueOrFalse: + active: true + EqualsWithHashCodeExist: + active: true + ExitOutsideMain: + active: false + ExplicitGarbageCollectionCall: + active: true + HasPlatformType: + active: true + IgnoredReturnValue: + active: true + restrictToConfig: true + returnValueAnnotations: + - 'CheckResult' + - '*.CheckResult' + - 'CheckReturnValue' + - '*.CheckReturnValue' + ignoreReturnValueAnnotations: + - 'CanIgnoreReturnValue' + - '*.CanIgnoreReturnValue' + returnValueTypes: + - 'kotlin.Function*' + - 'kotlin.sequences.Sequence' + - 'kotlinx.coroutines.flow.*Flow' + - 'java.util.stream.*Stream' + ignoreFunctionCall: [] + ImplicitDefaultLocale: + active: true + ImplicitUnitReturnType: + active: false + ignoreAnnotated: ['Test'] + allowExplicitReturnType: true + InvalidRange: + active: true + IteratorHasNextCallsNextMethod: + active: true + IteratorNotThrowingNoSuchElementException: + active: true + LateinitUsage: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + ignoreOnClassesPattern: '' + MapGetWithNotNullAssertionOperator: + active: true + MissingPackageDeclaration: + active: true + excludes: ['**/*.kts'] + MissingSuperCall: + active: false + mustInvokeSuperAnnotations: + - 'androidx.annotation.CallSuper' + - 'javax.annotation.OverridingMethodsMustInvokeSuper' + MissingUseCall: + active: false + ignoreClass: + - 'java.io.ByteArrayInputStream' + - 'java.io.ByteArrayOutputStream' + NullCheckOnMutableProperty: + active: false + NullableToStringCall: + active: false + PropertyUsedBeforeDeclaration: + active: false + UnconditionalJumpStatementInLoop: + active: false + UnnamedParameterUse: + active: false + allowAdjacentDifferentTypeParams: true + allowSingleParamUse: true + ignoreArgumentsMatchingNames: true + ignoreFunctionCall: [] + UnnecessaryNotNullCheck: + active: false + UnnecessaryNotNullOperator: + active: true + UnnecessarySafeCall: + active: true + UnreachableCatchBlock: + active: true + UnreachableCode: + active: true + UnsafeCallOnNullableType: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + UnsafeCast: + active: true + aliases: ['UNCHECKED_CAST'] + UnusedUnaryOperator: + active: true + UselessPostfixExpression: + active: true + WrongEqualsTypeParameter: + active: true + +style: + active: true + AbstractClassCanBeConcreteClass: + active: true + AbstractClassCanBeInterface: + active: true + AlsoCouldBeApply: + active: false + BracesOnIfStatements: + active: false + singleLine: 'never' + multiLine: 'always' + BracesOnWhenStatements: + active: false + singleLine: 'necessary' + multiLine: 'consistent' + CanBeNonNullable: + active: false + CascadingCallWrapping: + active: false + includeElvis: true + ClassOrdering: + active: false + CollapsibleIfStatements: + active: false + DataClassContainsFunctions: + active: false + conversionFunctionPrefix: + - 'to' + allowOperators: false + DataClassShouldBeImmutable: + active: false + DestructuringDeclarationWithTooManyEntries: + active: true + maxDestructuringEntries: 3 + DoubleNegativeExpression: + active: false + DoubleNegativeLambda: + active: false + negativeFunctions: + - reason: 'Use `takeIf` instead.' + value: 'takeUnless' + - reason: 'Use `all` instead.' + value: 'none' + negativeFunctionNameParts: + - 'not' + - 'non' + EqualsNullCall: + active: true + EqualsOnSignatureLine: + active: false + ExplicitCollectionElementAccessMethod: + active: false + ExplicitItLambdaMultipleParameters: + active: true + ExplicitItLambdaParameter: + active: false + ExpressionBodySyntax: + active: false + includeLineWrapping: false + ForbiddenAnnotation: + active: true + annotations: + - reason: 'it is a java annotation. Use `Suppress` instead.' + value: 'java.lang.SuppressWarnings' + - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.' + value: 'java.lang.Deprecated' + - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.' + value: 'java.lang.annotation.Documented' + - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.' + value: 'java.lang.annotation.Target' + - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.' + value: 'java.lang.annotation.Retention' + - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.' + value: 'java.lang.annotation.Repeatable' + ForbiddenComment: + active: true + comments: + - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.' + value: 'FIXME:' + - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.' + value: 'STOPSHIP:' + - reason: 'Forbidden TODO todo marker in comment, please do the changes.' + value: 'TODO:' + allowedPatterns: '' + ForbiddenImport: + active: false + forbiddenImports: [] + allowedImports: [] + ForbiddenMethodCall: + active: true + methods: + - reason: 'print does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.print' + - reason: 'println does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.println' + - reason: 'using `BigDecimal(Double)` can result in unexpected floating point precision behavior. Use `BigDecimal.valueOf(Double)` or `String.toBigDecimalOrNull()` instead.' + value: 'java.math.BigDecimal.(kotlin.Double)' + - reason: 'using `BigDecimal(String)` can result in a `NumberFormatException`. Use `String.toBigDecimalOrNull()`' + value: 'java.math.BigDecimal.(kotlin.String)' + - reason: 'It is marked as obsolete. Use `kotlin.time.measureTime` instead.' + value: 'kotlin.system.measureTimeMillis' + ForbiddenNamedParam: + active: false + methods: [] + ForbiddenOptIn: + active: false + markerClasses: [] + ForbiddenSuppress: + active: false + rules: [] + ForbiddenVoid: + active: true + ignoreOverridden: false + ignoreUsageInGenerics: false + FunctionOnlyReturningConstant: + active: true + ignoreOverridableFunction: true + ignoreActualFunction: true + excludedFunctions: [] + LoopWithTooManyJumpStatements: + active: true + maxJumpCount: 1 + MagicNumber: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts'] + ignoreNumbers: + - '-1' + - '0' + - '1' + - '2' + ignoreHashCodeFunction: true + ignorePropertyDeclaration: true + ignoreLocalVariableDeclaration: true + ignoreConstantDeclaration: true + ignoreCompanionObjectPropertyDeclaration: true + ignoreAnnotation: false + ignoreNamedArgument: true + ignoreEnums: false + ignoreRanges: false + ignoreExtensionFunctions: true + MandatoryBracesLoops: + active: false + MaxChainedCallsOnSameLine: + active: false + maxChainedCalls: 5 + MaxLineLength: + active: false + maxLineLength: 120 + excludePackageStatements: true + excludeImportStatements: true + excludeCommentStatements: false + excludeRawStrings: true + MayBeConstant: + active: true + ModifierOrder: + active: true + MultilineLambdaItParameter: + active: false + MultilineRawStringIndentation: + active: false + indentSize: 4 + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + NestedClassesVisibility: + active: true + NewLineAtEndOfFile: + active: true + NoTabs: + active: false + NullableBooleanCheck: + active: false + ObjectLiteralToLambda: + active: true + OptionalAbstractKeyword: + active: true + OptionalUnit: + active: false + ProtectedMemberInFinalClass: + active: true + RangeUntilInsteadOfRangeTo: + active: true + RedundantConstructorKeyword: + active: false + RedundantExplicitType: + active: false + RedundantHigherOrderMapUsage: + active: true + RedundantVisibilityModifier: + active: false + ReturnCount: + active: false + max: 2 + excludedFunctions: + - 'equals' + excludeLabeled: false + excludeReturnFromLambda: true + excludeGuardClauses: false + SafeCast: + active: true + SerialVersionUIDInSerializableClass: + active: true + SpacingAfterPackageAndImports: + active: false + StringShouldBeRawString: + active: false + maxEscapedCharacterCount: 2 + ignoredCharacters: [] + ThrowsCount: + active: false + max: 2 + excludeGuardClauses: false + TrailingWhitespace: + active: false + TrimMultilineRawString: + active: false + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + UnderscoresInNumericLiterals: + active: false + acceptableLength: 4 + allowNonStandardGrouping: false + UnnecessaryAny: + active: false + UnnecessaryApply: + active: true + UnnecessaryBackticks: + active: false + UnnecessaryBracesAroundTrailingLambda: + active: false + UnnecessaryFilter: + active: true + UnnecessaryFullyQualifiedName: + active: true + UnnecessaryInheritance: + active: true + UnnecessaryInnerClass: + active: false + UnnecessaryLet: + active: false + UnnecessaryParentheses: + active: false + allowForUnclearPrecedence: false + UnnecessaryReversed: + active: false + UnusedImport: + active: false + additionalOperatorSet: [] + UnusedParameter: + active: true + aliases: ['UNUSED_PARAMETER', 'unused'] + allowedNames: 'ignored|expected' + UnusedPrivateClass: + active: true + aliases: ['unused'] + UnusedPrivateFunction: + active: true + aliases: ['unused'] + allowedNames: '' + UnusedPrivateProperty: + active: true + aliases: ['unused'] + allowedNames: 'ignored|expected|serialVersionUID' + UnusedVariable: + active: true + aliases: ['UNUSED_VARIABLE', 'unused'] + allowedNames: 'ignored|_' + UseAnyOrNoneInsteadOfFind: + active: true + UseArrayLiteralsInAnnotations: + active: true + UseCheckNotNull: + active: true + UseCheckOrError: + active: true + UseDataClass: + active: false + allowVars: false + UseEmptyCounterpart: + active: false + UseIfEmptyOrIfBlank: + active: false + UseIfInsteadOfWhen: + active: false + ignoreWhenContainingVariableDeclaration: false + UseIsNullOrEmpty: + active: true + UseLet: + active: false + UseOrEmpty: + active: true + UseRequire: + active: true + UseRequireNotNull: + active: true + UseSumOfInsteadOfFlatMapSize: + active: false + UselessCallOnNotNull: + active: true + UtilityClassWithPublicConstructor: + active: true + VarCouldBeVal: + active: true + aliases: ['CanBeVal'] + ignoreLateinitVar: false + WildcardImport: + active: false + excludeImports: + - 'java.util.*' diff --git a/conformance-test/.gitignore b/conformance-test/.gitignore new file mode 100644 index 0000000..6628455 --- /dev/null +++ b/conformance-test/.gitignore @@ -0,0 +1 @@ +/results/ diff --git a/conformance-test/README.md b/conformance-test/README.md new file mode 100644 index 0000000..9b827bc --- /dev/null +++ b/conformance-test/README.md @@ -0,0 +1,118 @@ +# MCP Conformance Tests + +Conformance tests for the Kotlin MCP SDK. Uses the external +[`@modelcontextprotocol/conformance`](https://www.npmjs.com/package/@modelcontextprotocol/conformance) +runner (pinned to **0.1.15**) to validate compliance with the MCP specification. + +## Prerequisites + +- **JDK 17+** +- **Node.js 18+** and `npx` (for the conformance runner) +- **curl** (used to poll server readiness) + +## Quick Start + +Run **all** suites (server, client core, client auth) from the project root: + +```bash +./conformance-test/run-conformance.sh all +``` + +## Commands + +``` +./conformance-test/run-conformance.sh [extra-args...] +``` + +| Command | What it does | +|---------------|--------------------------------------------------------------------------------------| +| `list` | [List scenarios available in MCP Conformance Test Framework][list-scenarios-command] | +| `server` | Starts the Ktor conformance server, runs the server test suite against it | +| `client` | Runs the client test suite (`initialize`, `tools_call`, `elicitation`, `sse-retry`) | +| `client-auth` | Runs the client auth test suite (20 OAuth scenarios) | +| `all` | Runs all three suites sequentially | + +Any `[extra-args]` are forwarded to the conformance runner (e.g. `--verbose`). + +## What the Script Does + +1. **Builds** the module via `./gradlew :conformance-test:installDist` +2. For `server` — starts the conformance server on `localhost:3001`, polls until ready +3. Invokes `npx @modelcontextprotocol/conformance@0.1.15` with the appropriate arguments +4. Saves results to `conformance-test/results//` +5. Cleans up the server process on exit +6. Exits non-zero if any suite fails + +## Environment Variables + +| Variable | Default | Description | +|------------|---------|---------------------------------| +| `MCP_PORT` | `3001` | Port for the conformance server | + +## Project Structure + +``` +conformance-test/ +├── run-conformance.sh # Single entry point script +├── conformance-baseline.yml # Expected failures for known SDK limitations +└── src/main/kotlin/.../conformance/ + ├── ConformanceServer.kt # Ktor server entry point (StreamableHTTP, DNS rebinding, EventStore) + ├── ConformanceClient.kt # Scenario-based client entry point (MCP_CONFORMANCE_SCENARIO routing) + ├── ConformanceTools.kt # 18 tool registrations + ├── ConformanceResources.kt # 5 resource registrations (static, binary, template, watched, dynamic) + ├── ConformancePrompts.kt # 5 prompt registrations (simple, args, image, embedded, dynamic) + ├── ConformanceCompletions.kt # completion/complete handler + ├── InMemoryEventStore.kt # EventStore impl for SSE resumability (SEP-1699) + └── auth/ # OAuth client for 20 auth scenarios + ├── registration.kt # Scenario handler registration + ├── utils.kt # Shared utilities: JSON instance, constants, extractOrigin() + ├── discovery.kt # Protected Resource Metadata + AS Metadata discovery + ├── pkce.kt # PKCE code verifier/challenge generation + AS capability check + ├── tokenExchange.kt # Token endpoint interaction (exchange code, error handling) + ├── authCodeFlow.kt # Main Authorization Code flow handler (runAuthClient + interceptor) + ├── scopeHandling.kt # Scope selection strategy + step-up 403 handling + ├── clientRegistration.kt # Client registration logic (pre-reg, CIMD, dynamic) + ├── JWTScenario.kt # Client Credentials JWT scenario + ├── basicScenario.kt # Client Credentials Basic scenario + └── crossAppAccessScenario.kt # Cross-App Access (SEP-990) scenario +``` + +## Test Suites + +### Server Suite + +Tests the conformance server against all server scenarios: + +| Category | Scenarios | +|-------------|-------------------------------------------------------------------------------------------------------------------------------------| +| Lifecycle | initialize, ping | +| Tools | text, image, audio, embedded, multiple, progress, logging, error, sampling, elicitation, dynamic, reconnection, JSON Schema 2020-12 | +| Resources | list, read-text, read-binary, templates, subscribe, dynamic | +| Prompts | simple, with-args, with-image, with-embedded-resource, dynamic | +| Completions | complete | +| Security | DNS rebinding protection | + +### Client Core Suite + +| Scenario | Description | +|---------------------------------------|-----------------------------------------------| +| `initialize` | Connect, list tools, close | +| `tools_call` | Connect, call `add_numbers(a=5, b=3)`, close | +| `elicitation-sep1034-client-defaults` | Elicitation with `applyDefaults` capability | +| `sse-retry` | Call `test_reconnection`, verify reconnection | + +### Client Auth Suite + +17 OAuth Authorization Code scenarios + 2 Client Credentials scenarios (`jwt`, `basic`) + 1 Cross-App Access scenario = 20 total. + +> [!NOTE] +> Auth scenarios are implemented using Ktor's `HttpClient` plugins (`HttpSend` interceptor, +> `ktor-client-auth`) as a standalone OAuth client. They do not use the SDK's built-in auth support. + +## Known SDK Limitations + +Scenarios, expected to fail due to current SDK limitations, are tracked in +[`conformance-baseline.yml`](conformance-baseline.yml). +These failures reveal SDK gaps and are intentionally not fixed in this module. + +[list-scenarios-command]: https://github.com/modelcontextprotocol/conformance/tree/main?tab=readme-ov-file#list-available-scenarios diff --git a/conformance-test/build.gradle.kts b/conformance-test/build.gradle.kts new file mode 100644 index 0000000..a96f4fb --- /dev/null +++ b/conformance-test/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + kotlin("jvm") + application +} + +application { + mainClass.set("io.modelcontextprotocol.kotlin.sdk.conformance.ConformanceServerKt") +} + +tasks.register("conformanceClientScripts") { + mainClass.set("io.modelcontextprotocol.kotlin.sdk.conformance.ConformanceClientKt") + applicationName = "conformance-client" + outputDir = tasks.named("startScripts").get().outputDir + classpath = tasks.named("jar").get().outputs.files + configurations.named("runtimeClasspath").get() +} + +tasks.named("installDist") { + dependsOn("conformanceClientScripts") +} + +tasks.named("clean") { + delete("results") +} + +dependencies { + implementation(project(":kotlin-sdk")) + implementation(libs.ktor.server.cio) + implementation(libs.ktor.server.content.negotiation) + implementation(libs.ktor.serialization) + implementation(libs.ktor.client.cio) + implementation(libs.ktor.client.auth) + implementation(libs.kotlin.logging) + runtimeOnly(libs.slf4j.simple) +} diff --git a/conformance-test/conformance-baseline.yml b/conformance-test/conformance-baseline.yml new file mode 100644 index 0000000..f349b8c --- /dev/null +++ b/conformance-test/conformance-baseline.yml @@ -0,0 +1,5 @@ +# Conformance test baseline - expected failures +# Add entries here as tests are identified as known SDK limitations +server: [] + +client: [] diff --git a/conformance-test/run-conformance.sh b/conformance-test/run-conformance.sh new file mode 100755 index 0000000..279254e --- /dev/null +++ b/conformance-test/run-conformance.sh @@ -0,0 +1,194 @@ +#!/bin/bash +# Script to run MCP conformance tests for the Kotlin SDK. +# +# Usage: ./conformance-test/run-conformance.sh [extra-args...] +# Commands: server | client | client-auth | all + +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" || exit 1; pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." || exit 1; pwd)" + +CONFORMANCE_VERSION="0.1.16" +PORT="${MCP_PORT:-3001}" +SERVER_URL="http://localhost:${PORT}/mcp" +RESULTS_DIR="$SCRIPT_DIR/results" +SERVER_DIST="$SCRIPT_DIR/build/install/conformance-test/bin/conformance-test" +CLIENT_DIST="$SCRIPT_DIR/build/install/conformance-test/bin/conformance-client" + +SERVER_PID="" + +# shellcheck disable=SC2317 +cleanup() { + if [ -n "$SERVER_PID" ] && kill -0 "$SERVER_PID" 2>/dev/null; then + echo "Stopping server (PID: $SERVER_PID)..." + kill "$SERVER_PID" 2>/dev/null || true + wait "$SERVER_PID" 2>/dev/null || true + fi +} +trap cleanup EXIT + +build() { + echo "Building conformance-test distributions..." + cd "$PROJECT_ROOT" || return 1 + ./gradlew :conformance-test:installDist --quiet + cd "$SCRIPT_DIR" || return 1 + echo "Build complete." +} + +start_server() { + echo "Starting conformance server on port $PORT..." + MCP_PORT="$PORT" "$SERVER_DIST" & + SERVER_PID=$! + + echo "Waiting for server to be ready..." + local retries=0 + local max_retries=30 + while ! curl -sf "$SERVER_URL" > /dev/null 2>&1; do + retries=$((retries + 1)) + if [ "$retries" -ge "$max_retries" ]; then + echo "ERROR: Server failed to start after $max_retries attempts" + return 1 + fi + sleep 0.5 + done + echo "Server is ready (PID: $SERVER_PID)." +} + +stop_server() { + if [ -n "$SERVER_PID" ] && kill -0 "$SERVER_PID" 2>/dev/null; then + echo "Stopping server (PID: $SERVER_PID)..." + kill "$SERVER_PID" 2>/dev/null || true + wait "$SERVER_PID" 2>/dev/null || true + SERVER_PID="" + fi +} + +run_list_scenarios() { + local output_dir="$RESULTS_DIR/list" + mkdir -p "$output_dir" + echo "" + echo "==========================================" + echo " List Available Scenarios" + echo "==========================================" + local rc=0 + npx "@modelcontextprotocol/conformance@$CONFORMANCE_VERSION" list \ + "$@" > "$output_dir/scenarios.txt" || rc=$? + + cat "$output_dir/scenarios.txt" + return $rc +} + +run_server_suite() { + local output_dir="$RESULTS_DIR/server" + mkdir -p "$output_dir" + echo "" + echo "==========================================" + echo " Running SERVER conformance tests" + echo "==========================================" + start_server || return 1 + local rc=0 + npx "@modelcontextprotocol/conformance@$CONFORMANCE_VERSION" server \ + --url "$SERVER_URL" \ + --output-dir "$output_dir" \ + --expected-failures "$SCRIPT_DIR/conformance-baseline.yml" \ + "$@" || rc=$? + stop_server + return $rc +} + +run_client_suite() { + local output_dir="$RESULTS_DIR/client" + mkdir -p "$output_dir" + echo "" + echo "==========================================" + echo " Running CLIENT conformance tests" + echo "==========================================" + local scenarios=("initialize" "tools_call" "elicitation-sep1034-client-defaults" "sse-retry") + local rc=0 + for scenario in "${scenarios[@]}"; do + npx "@modelcontextprotocol/conformance@$CONFORMANCE_VERSION" client \ + --command "$CLIENT_DIST" \ + --scenario "$scenario" \ + --output-dir "$output_dir" \ + --expected-failures "$SCRIPT_DIR/conformance-baseline.yml" \ + "$@" || rc=$? + done + return $rc +} + +run_client_auth_suite() { + local output_dir="$RESULTS_DIR/client-auth" + mkdir -p "$output_dir" + echo "" + echo "==========================================" + echo " Running CLIENT (auth) conformance tests" + echo "==========================================" + local rc=0 + npx "@modelcontextprotocol/conformance@$CONFORMANCE_VERSION" client \ + --command "$CLIENT_DIST" \ + --suite auth \ + --output-dir "$output_dir" \ + --expected-failures "$SCRIPT_DIR/conformance-baseline.yml" \ + "$@" || rc=$? + + local extra_scenarios=("auth/client-credentials-jwt" "auth/client-credentials-basic" "auth/cross-app-access-complete-flow") + for scenario in "${extra_scenarios[@]}"; do + npx "@modelcontextprotocol/conformance@$CONFORMANCE_VERSION" client \ + --command "$CLIENT_DIST" \ + --scenario "$scenario" \ + --output-dir "$output_dir" \ + --expected-failures "$SCRIPT_DIR/conformance-baseline.yml" \ + "$@" || rc=$? + done + return $rc +} + +# ============================================================================ +# Main +# ============================================================================ + +COMMAND="${1:-}" +shift 2>/dev/null || true + +if [ -z "$COMMAND" ]; then + echo "Usage: $0 [extra-args...]" + echo "Commands: list | server | client | client-auth | all" + exit 1 +fi + +build + +EXIT_CODE=0 + +case "$COMMAND" in + list) + run_list_scenarios "$@" || EXIT_CODE=1 + ;; + server) + run_server_suite "$@" || EXIT_CODE=1 + ;; + client) + run_client_suite "$@" || EXIT_CODE=1 + ;; + client-auth) + run_client_auth_suite "$@" || EXIT_CODE=1 + ;; + all) + run_server_suite "$@" || EXIT_CODE=1 + run_client_suite "$@" || EXIT_CODE=1 + run_client_auth_suite "$@" || EXIT_CODE=1 + ;; + *) + echo "Unknown command: $COMMAND" + echo "Commands: list | server | client | client-auth | all" + exit 1 + ;; +esac + +echo "" +echo "==========================================" +echo " Results saved to: $RESULTS_DIR" +echo "==========================================" + +exit $EXIT_CODE diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceClient.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceClient.kt new file mode 100644 index 0000000..dba24ff --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceClient.kt @@ -0,0 +1,199 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance + +import io.github.oshai.kotlinlogging.KotlinLogging +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.sse.SSE +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions +import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport +import io.modelcontextprotocol.kotlin.sdk.conformance.auth.registerAuthScenarios +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.ElicitResult +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlin.system.exitProcess +import kotlin.time.Duration.Companion.seconds + +private val logger = KotlinLogging.logger {} + +typealias ScenarioHandler = suspend (serverUrl: String) -> Unit + +internal val scenarioHandlers = mutableMapOf() + +// ============================================================================ +// Main entry point +// ============================================================================ + +fun main(args: Array) { + val scenarioName = System.getenv("MCP_CONFORMANCE_SCENARIO") + val serverUrl = args.lastOrNull() + + // Register all scenario handlers + registerCoreScenarios() + registerAuthScenarios() + + if (scenarioName == null || serverUrl == null) { + logger.error { "Usage: MCP_CONFORMANCE_SCENARIO= conformance-client " } + logger.error { "\nThe MCP_CONFORMANCE_SCENARIO env var is set automatically by the conformance runner." } + logger.error { "\nAvailable scenarios:" } + for (name in scenarioHandlers.keys.sorted()) { + logger.error { " - $name" } + } + exitProcess(1) + } + + val handler = scenarioHandlers[scenarioName] + if (handler == null) { + logger.error { "Unknown scenario: $scenarioName" } + logger.error { "\nAvailable scenarios:" } + for (name in scenarioHandlers.keys.sorted()) { + logger.error { " - $name" } + } + exitProcess(1) + } + + try { + runBlocking { + handler(serverUrl) + } + exitProcess(0) + } catch (e: Exception) { + logger.error(e) { "Error: ${e.message}" } + exitProcess(1) + } +} + +// ============================================================================ +// Shared HTTP client factory +// ============================================================================ + +private fun createHttpClient(): HttpClient = HttpClient(CIO) { + install(SSE) + install(HttpTimeout) { + requestTimeoutMillis = 30.seconds.inWholeMilliseconds + } +} + +// ============================================================================ +// Basic scenarios (initialize, tools_call) +// ============================================================================ + +private suspend fun runBasicClient(serverUrl: String) { + createHttpClient().use { httpClient -> + val transport = StreamableHttpClientTransport(httpClient, serverUrl) + val client = Client( + clientInfo = Implementation("test-client", "1.0.0"), + options = ClientOptions(capabilities = ClientCapabilities()), + ) + client.connect(transport) + client.close() + } +} + +private suspend fun runToolsCallClient(serverUrl: String) { + createHttpClient().use { httpClient -> + val transport = StreamableHttpClientTransport(httpClient, serverUrl) + val client = Client( + clientInfo = Implementation("test-client", "1.0.0"), + options = ClientOptions(capabilities = ClientCapabilities()), + ) + client.connect(transport) + + val tools = client.listTools() + val addTool = tools.tools.find { it.name == "add_numbers" } + if (addTool != null) { + client.callTool( + CallToolRequest( + CallToolRequestParams( + name = "add_numbers", + arguments = buildJsonObject { + put("a", 5) + put("b", 3) + }, + ), + ), + ) + } + + client.close() + } +} + +// ============================================================================ +// Elicitation defaults scenario +// ============================================================================ + +private suspend fun runElicitationDefaultsClient(serverUrl: String) { + createHttpClient().use { httpClient -> + val transport = StreamableHttpClientTransport(httpClient, serverUrl) + val client = Client( + clientInfo = Implementation("elicitation-defaults-test-client", "1.0.0"), + options = ClientOptions( + capabilities = ClientCapabilities( + elicitation = ClientCapabilities.elicitation, + ), + ), + ) + + // Register elicitation handler that returns empty content — SDK should fill in defaults + client.setElicitationHandler { _ -> + ElicitResult( + action = ElicitResult.Action.Accept, + content = JsonObject(emptyMap()), + ) + } + + client.connect(transport) + + val tools = client.listTools() + val testTool = tools.tools.find { it.name == "test_client_elicitation_defaults" } + ?: error("Test tool not found: test_client_elicitation_defaults") + + client.callTool( + CallToolRequest(CallToolRequestParams(name = testTool.name)), + ) + + client.close() + } +} + +// ============================================================================ +// SSE retry scenario +// ============================================================================ + +private suspend fun runSSERetryClient(serverUrl: String) { + createHttpClient().use { httpClient -> + val transport = StreamableHttpClientTransport(httpClient, serverUrl) + val client = Client( + clientInfo = Implementation("sse-retry-test-client", "1.0.0"), + options = ClientOptions(capabilities = ClientCapabilities()), + ) + client.connect(transport) + + client.listTools() + + client.callTool( + CallToolRequest(CallToolRequestParams(name = "test_reconnection")), + ) + + client.close() + } +} + +// ============================================================================ +// Register core scenarios +// ============================================================================ + +private fun registerCoreScenarios() { + scenarioHandlers["initialize"] = ::runBasicClient + scenarioHandlers["tools_call"] = ::runToolsCallClient + scenarioHandlers["elicitation-sep1034-client-defaults"] = ::runElicitationDefaultsClient + scenarioHandlers["sse-retry"] = ::runSSERetryClient +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceCompletions.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceCompletions.kt new file mode 100644 index 0000000..6297139 --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceCompletions.kt @@ -0,0 +1,15 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance + +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.types.CompleteRequest +import io.modelcontextprotocol.kotlin.sdk.types.CompleteResult +import io.modelcontextprotocol.kotlin.sdk.types.Method + +fun Server.registerConformanceCompletions() { + onConnect { + val session = sessions.values.lastOrNull() ?: return@onConnect + session.setRequestHandler(Method.Defined.CompletionComplete) { _, _ -> + CompleteResult(CompleteResult.Completion(values = emptyList(), total = 0, hasMore = false)) + } + } +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformancePrompts.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformancePrompts.kt new file mode 100644 index 0000000..6c591e4 --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformancePrompts.kt @@ -0,0 +1,114 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance + +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.types.EmbeddedResource +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult +import io.modelcontextprotocol.kotlin.sdk.types.ImageContent +import io.modelcontextprotocol.kotlin.sdk.types.PromptArgument +import io.modelcontextprotocol.kotlin.sdk.types.PromptMessage +import io.modelcontextprotocol.kotlin.sdk.types.Role +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents +import kotlinx.coroutines.delay +import kotlin.time.Duration.Companion.milliseconds + +fun Server.registerConformancePrompts() { + // 1. Simple prompt + addPrompt( + name = "test_simple_prompt", + description = "test_simple_prompt", + ) { + GetPromptResult( + messages = listOf( + PromptMessage(Role.User, TextContent("This is a simple prompt for testing.")), + ), + ) + } + + // 2. Prompt with arguments + addPrompt( + name = "test_prompt_with_arguments", + description = "test_prompt_with_arguments", + arguments = listOf( + PromptArgument(name = "arg1", description = "First test argument", required = true), + PromptArgument(name = "arg2", description = "Second test argument", required = true), + ), + ) { request -> + val arg1 = request.arguments?.get("arg1") ?: "" + val arg2 = request.arguments?.get("arg2") ?: "" + GetPromptResult( + messages = listOf( + PromptMessage( + Role.User, + TextContent("Prompt with arguments: arg1='$arg1', arg2='$arg2'"), + ), + ), + ) + } + + // 3. Prompt with image + addPrompt( + name = "test_prompt_with_image", + description = "test_prompt_with_image", + ) { + GetPromptResult( + messages = listOf( + PromptMessage(Role.User, ImageContent(data = PNG_BASE64, mimeType = "image/png")), + PromptMessage(Role.User, TextContent("Please analyze the image above.")), + ), + ) + } + + // 4. Prompt with embedded resource + addPrompt( + name = "test_prompt_with_embedded_resource", + description = "test_prompt_with_embedded_resource", + arguments = listOf( + PromptArgument(name = "resourceUri", description = "URI of the resource to embed", required = true), + ), + ) { request -> + val resourceUri = request.arguments?.get("resourceUri") ?: "test://embedded-resource" + GetPromptResult( + messages = listOf( + PromptMessage( + Role.User, + EmbeddedResource( + resource = TextResourceContents( + text = "Embedded resource content for testing", + uri = resourceUri, + mimeType = "text/plain", + ), + ), + ), + PromptMessage(Role.User, TextContent("Please process the embedded resource above.")), + ), + ) + } + + // 5. Dynamic prompt + val server = this + addPrompt( + name = "test_dynamic_prompt", + description = "test_dynamic_prompt", + ) { + // Add a temporary prompt, triggering listChanged + server.addPrompt( + name = "test_dynamic_prompt_temp", + description = "Temporary dynamic prompt", + ) { + GetPromptResult( + messages = listOf( + PromptMessage(Role.User, TextContent("Temporary prompt response")), + ), + ) + } + delay(100.milliseconds) + // Remove the temporary prompt, triggering listChanged again + server.removePrompt("test_dynamic_prompt_temp") + GetPromptResult( + messages = listOf( + PromptMessage(Role.User, TextContent("Dynamic prompt executed successfully")), + ), + ) + } +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceResources.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceResources.kt new file mode 100644 index 0000000..89d83f6 --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceResources.kt @@ -0,0 +1,122 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance + +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.types.BlobResourceContents +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult +import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents +import kotlinx.coroutines.delay +import kotlin.time.Duration.Companion.milliseconds + +fun Server.registerConformanceResources() { + // 1. Static text resource + addResource( + uri = "test://static-text", + name = "static-text", + description = "A static text resource for testing", + mimeType = "text/plain", + ) { + ReadResourceResult( + listOf( + TextResourceContents( + text = "This is the content of the static text resource.", + uri = "test://static-text", + mimeType = "text/plain", + ), + ), + ) + } + + // 2. Static binary resource + addResource( + uri = "test://static-binary", + name = "static-binary", + description = "A static binary resource for testing", + mimeType = "image/png", + ) { + ReadResourceResult( + listOf( + BlobResourceContents( + blob = PNG_BASE64, + uri = "test://static-binary", + mimeType = "image/png", + ), + ), + ) + } + + // 3. Template resource + addResourceTemplate( + uriTemplate = "test://template/{id}/data", + name = "template", + description = "A template resource for testing", + mimeType = "application/json", + ) { request, variables -> + val id = variables["id"] + ReadResourceResult( + listOf( + TextResourceContents( + text = """{"id": "$id"}""", + uri = request.uri, + mimeType = "application/json", + ), + ), + ) + } + + // 4. Watched resource + addResource( + uri = "test://watched-resource", + name = "watched-resource", + description = "A watched resource for testing", + mimeType = "text/plain", + ) { + ReadResourceResult( + listOf( + TextResourceContents( + text = "Watched resource content.", + uri = "test://watched-resource", + mimeType = "text/plain", + ), + ), + ) + } + + // 5. Dynamic resource + val server = this + addResource( + uri = "test://dynamic-resource", + name = "dynamic-resource", + description = "A dynamic resource for testing", + mimeType = "text/plain", + ) { + // Add a temporary resource, triggering listChanged + server.addResource( + uri = "test://dynamic-resource-temp", + name = "dynamic-resource-temp", + description = "Temporary dynamic resource", + mimeType = "text/plain", + ) { + ReadResourceResult( + listOf( + TextResourceContents( + text = "Temporary resource content.", + uri = "test://dynamic-resource-temp", + mimeType = "text/plain", + ), + ), + ) + } + delay(100.milliseconds) + // Remove the temporary resource, triggering listChanged again + server.removeResource("test://dynamic-resource-temp") + ReadResourceResult( + listOf( + TextResourceContents( + text = "Dynamic resource content.", + uri = "test://dynamic-resource", + mimeType = "text/plain", + ), + ), + ) + } +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceServer.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceServer.kt new file mode 100644 index 0000000..8a18957 --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceServer.kt @@ -0,0 +1,38 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance + +import io.ktor.server.cio.CIO +import io.ktor.server.engine.embeddedServer +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import io.modelcontextprotocol.kotlin.sdk.server.mcpStreamableHttp +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities + +fun main() { + val port = System.getenv("MCP_PORT")?.toIntOrNull() ?: 3001 + embeddedServer(CIO, port = port) { + mcpStreamableHttp( + eventStore = InMemoryEventStore(), + ) { + createConformanceServer() + } + }.start(wait = true) +} + +fun createConformanceServer(): Server = Server( + serverInfo = Implementation("mcp-kotlin-sdk-conformance", "0.1.0"), + options = ServerOptions( + ServerCapabilities( + tools = ServerCapabilities.Tools(listChanged = true), + resources = ServerCapabilities.Resources(subscribe = true, listChanged = true), + prompts = ServerCapabilities.Prompts(listChanged = true), + logging = ServerCapabilities.Logging, + completions = ServerCapabilities.Completions, + ), + ), +) { + registerConformanceTools() + registerConformanceResources() + registerConformancePrompts() + registerConformanceCompletions() +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt new file mode 100644 index 0000000..c81c98c --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt @@ -0,0 +1,454 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance + +import io.github.oshai.kotlinlogging.KotlinLogging +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.types.AudioContent +import io.modelcontextprotocol.kotlin.sdk.types.BooleanSchema +import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.DoubleSchema +import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.EmbeddedResource +import io.modelcontextprotocol.kotlin.sdk.types.EnumOption +import io.modelcontextprotocol.kotlin.sdk.types.ImageContent +import io.modelcontextprotocol.kotlin.sdk.types.IntegerSchema +import io.modelcontextprotocol.kotlin.sdk.types.LegacyTitledEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel +import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification +import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotificationParams +import io.modelcontextprotocol.kotlin.sdk.types.ProgressNotification +import io.modelcontextprotocol.kotlin.sdk.types.ProgressNotificationParams +import io.modelcontextprotocol.kotlin.sdk.types.Role +import io.modelcontextprotocol.kotlin.sdk.types.SamplingMessage +import io.modelcontextprotocol.kotlin.sdk.types.StringSchema +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents +import io.modelcontextprotocol.kotlin.sdk.types.TitledMultiSelectEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.TitledSingleSelectEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema +import io.modelcontextprotocol.kotlin.sdk.types.UntitledMultiSelectEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.UntitledSingleSelectEnumSchema +import kotlinx.coroutines.delay +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.double +import kotlinx.serialization.json.jsonPrimitive +import kotlin.time.Duration.Companion.milliseconds + +// Minimal 1x1 PNG (base64) +internal const val PNG_BASE64 = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==" + +// Minimal WAV (base64) +internal const val WAV_BASE64 = "UklGRiYAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQIAAAA=" + +private val logger = KotlinLogging.logger {} + +fun Server.registerConformanceTools() { + // 1. Simple text + addTool( + name = "test_simple_text", + description = "test_simple_text", + ) { + CallToolResult(listOf(TextContent("Simple text content"))) + } + + // 2. Image content + addTool( + name = "test_image_content", + description = "test_image_content", + ) { + CallToolResult(listOf(ImageContent(data = PNG_BASE64, mimeType = "image/png"))) + } + + // 3. Audio content + addTool( + name = "test_audio_content", + description = "test_audio_content", + ) { + CallToolResult(listOf(AudioContent(data = WAV_BASE64, mimeType = "audio/wav"))) + } + + // 4. Embedded resource + addTool( + name = "test_embedded_resource", + description = "test_embedded_resource", + ) { + CallToolResult( + listOf( + EmbeddedResource( + resource = TextResourceContents( + text = "This is an embedded resource content.", + uri = "test://embedded-resource", + mimeType = "text/plain", + ), + ), + ), + ) + } + + // 5. Multiple content types + addTool( + name = "test_multiple_content_types", + description = "test_multiple_content_types", + ) { + CallToolResult( + listOf( + TextContent("Simple text content"), + ImageContent(data = PNG_BASE64, mimeType = "image/png"), + EmbeddedResource( + resource = TextResourceContents( + text = "This is an embedded resource content.", + uri = "test://embedded-resource", + mimeType = "text/plain", + ), + ), + ), + ) + } + + // 6. Progress tool + addTool( + name = "test_tool_with_progress", + description = "test_tool_with_progress", + ) { request -> + val progressToken = request.meta?.progressToken + if (progressToken != null) { + notification( + ProgressNotification( + ProgressNotificationParams( + progressToken, + 0.0, + 100.0, + "Completed step 0 of 100", + ), + ), + ) + delay(50.milliseconds) + notification( + ProgressNotification( + ProgressNotificationParams( + progressToken, + 50.0, + 100.0, + "Completed step 50 of 100", + ), + ), + ) + delay(50.milliseconds) + notification( + ProgressNotification( + ProgressNotificationParams( + progressToken, + 100.0, + 100.0, + "Completed step 100 of 100", + ), + ), + ) + } + CallToolResult(listOf(TextContent("Simple text content"))) + } + + // 7. Error handling + addTool( + name = "test_error_handling", + description = "test_error_handling", + ) { + throw Exception("This tool intentionally returns an error for testing") + } + + // 8. Sampling + addTool( + name = "test_sampling", + description = "test_sampling", + inputSchema = ToolSchema( + properties = buildJsonObject { + put("prompt", buildJsonObject { put("type", JsonPrimitive("string")) }) + }, + required = listOf("prompt"), + ), + ) { request -> + val prompt = request.arguments?.get("prompt")?.jsonPrimitive?.content ?: "Hello" + val result = createMessage( + CreateMessageRequest( + CreateMessageRequestParams( + maxTokens = 10000, + messages = listOf(SamplingMessage(Role.User, listOf(TextContent(prompt)))), + ), + ), + ) + val sampledText = result.content.joinToString("\n") { content -> + when (content) { + is TextContent -> content.text + else -> "Non-text sampling content: ${content::class.simpleName}" + } + } + CallToolResult(listOf(TextContent(sampledText))) + } + + // 9. Elicitation + addTool( + name = "test_elicitation", + description = "test_elicitation", + inputSchema = ToolSchema( + properties = buildJsonObject { + put("message", buildJsonObject { put("type", JsonPrimitive("string")) }) + }, + required = listOf("message"), + ), + ) { request -> + val message = request.arguments?.get("message")?.jsonPrimitive?.content ?: "Please provide input" + val schema = ElicitRequestParams.RequestedSchema( + properties = mapOf( + "username" to StringSchema(description = "User's response"), + "email" to StringSchema(description = "User's email address"), + ), + required = listOf("username", "email"), + ) + val result = createElicitation(message, schema) + CallToolResult(listOf(TextContent("User response: "))) + } + + // 10. Elicitation SEP1034 (defaults) + addTool( + name = "test_elicitation_sep1034_defaults", + description = "test_elicitation_sep1034_defaults", + ) { + val schema = ElicitRequestParams.RequestedSchema( + properties = mapOf( + "name" to StringSchema(description = "User name", default = "John Doe"), + "age" to IntegerSchema(description = "User age", default = 30), + "score" to DoubleSchema(description = "User score", default = 95.5), + "status" to UntitledSingleSelectEnumSchema( + description = "User status", + enumValues = listOf("active", "inactive", "pending"), + default = "active", + ), + "verified" to BooleanSchema(description = "Verification status", default = true), + ), + required = listOf("name", "age", "score", "status", "verified"), + ) + val result = createElicitation( + "Please review and update the form fields with defaults", + schema, + ) + CallToolResult(listOf(TextContent(result.content.toString()))) + } + + // 11. Elicitation SEP1330 enums + addTool( + name = "test_elicitation_sep1330_enums", + description = "test_elicitation_sep1330_enums", + ) { + val schema = ElicitRequestParams.RequestedSchema( + properties = mapOf( + // Untitled single-select + "untitledSingle" to UntitledSingleSelectEnumSchema( + enumValues = listOf("option1", "option2", "option3"), + ), + // Titled single-select + "titledSingle" to TitledSingleSelectEnumSchema( + oneOf = listOf( + EnumOption(const = "value1", title = "First Option"), + EnumOption(const = "value2", title = "Second Option"), + EnumOption(const = "value3", title = "Third Option"), + ), + ), + // Legacy titled (deprecated) — uses enum + enumNames + @Suppress("DEPRECATION") + "legacyEnum" + to LegacyTitledEnumSchema( + enumValues = listOf("opt1", "opt2", "opt3"), + enumNames = listOf("Option One", "Option Two", "Option Three"), + ), + // Untitled multi-select + "untitledMulti" to UntitledMultiSelectEnumSchema( + items = UntitledMultiSelectEnumSchema.Items( + enumValues = listOf("option1", "option2", "option3"), + ), + ), + // Titled multi-select + "titledMulti" to TitledMultiSelectEnumSchema( + items = TitledMultiSelectEnumSchema.Items( + anyOf = listOf( + EnumOption(const = "value1", title = "First Choice"), + EnumOption(const = "value2", title = "Second Choice"), + EnumOption(const = "value3", title = "Third Choice"), + ), + ), + ), + ), + ) + val result = createElicitation( + "Please review and update the form fields with defaults", + schema, + ) + CallToolResult(listOf(TextContent("Elicitation completed: action=${result.action}, content=${result.content}"))) + } + + // 12. Dynamic tool + val server = this + addTool( + name = "test_dynamic_tool", + description = "test_dynamic_tool", + ) { + // Add a temporary tool, triggering listChanged + server.addTool( + name = "test_dynamic_tool_temp", + description = "Temporary dynamic tool", + ) { + CallToolResult(listOf(TextContent("Temporary tool response"))) + } + delay(100.milliseconds) + // Remove the temporary tool, triggering listChanged again + server.removeTool("test_dynamic_tool_temp") + CallToolResult(listOf(TextContent("Dynamic tool executed successfully"))) + } + + // 13. Logging tool + addTool( + name = "test_tool_with_logging", + description = "test_tool_with_logging", + ) { + logger.debug { "[test_tool_with_logging] Sending message 1" } + sendLoggingMessage( + LoggingMessageNotification( + LoggingMessageNotificationParams( + level = LoggingLevel.Info, + data = JsonPrimitive("Tool execution started"), + logger = "conformance", + ), + ), + ) + delay(50.milliseconds) + logger.debug { "[test_tool_with_logging] Sending message #2" } + sendLoggingMessage( + LoggingMessageNotification( + LoggingMessageNotificationParams( + level = LoggingLevel.Info, + data = JsonPrimitive("Tool processing data"), + logger = "conformance", + ), + ), + ) + delay(50.milliseconds) + logger.debug { "[test_tool_with_logging] Sending message 3" } + sendLoggingMessage( + LoggingMessageNotification( + LoggingMessageNotificationParams( + level = LoggingLevel.Info, + data = JsonPrimitive("Tool execution completed"), + logger = "conformance", + ), + ), + ) + + CallToolResult(listOf(TextContent("Simple text content"))) + } + + // 14. add_numbers — used by tools_call client scenario + addTool( + name = "add_numbers", + description = "Adds two numbers together", + inputSchema = ToolSchema( + properties = buildJsonObject { + put("a", buildJsonObject { put("type", JsonPrimitive("number")) }) + put("b", buildJsonObject { put("type", JsonPrimitive("number")) }) + }, + required = listOf("a", "b"), + ), + ) { request -> + val a = request.arguments?.get("a")?.jsonPrimitive?.double ?: 0.0 + val b = request.arguments?.get("b")?.jsonPrimitive?.double ?: 0.0 + val sum = a + b + CallToolResult(listOf(TextContent("The sum of $a and $b is $sum"))) + } + + // 15. test_reconnection — SEP-1699, closes SSE stream to test client reconnection + addTool( + name = "test_reconnection", + description = "Tests SSE stream disconnection and client reconnection (SEP-1699)", + ) { + // SDK limitation: cannot access the JSONRPC request ID from the tool handler + // to close the SSE stream. Return success text; this test may fail at the + // conformance runner level because the stream isn't actually closed. + delay(100.milliseconds) + CallToolResult( + listOf( + TextContent( + "Reconnection test completed successfully. " + + "If you received this, the client properly reconnected after stream closure.", + ), + ), + ) + } + + // 16. json_schema_2020_12_tool — SEP-1613 + addTool( + name = "json_schema_2020_12_tool", + description = "Tool with JSON Schema 2020-12 features for conformance testing (SEP-1613)", + inputSchema = ToolSchema( + properties = buildJsonObject { + put( + "name", + buildJsonObject { + put("type", JsonPrimitive("string")) + }, + ) + put( + "address", + buildJsonObject { + put("type", JsonPrimitive("object")) + put( + "properties", + buildJsonObject { + put("street", buildJsonObject { put("type", JsonPrimitive("string")) }) + put("city", buildJsonObject { put("type", JsonPrimitive("string")) }) + }, + ) + }, + ) + }, + ), + ) { request -> + CallToolResult( + listOf(TextContent("JSON Schema 2020-12 tool called with: ${request.arguments}")), + ) + } + + // 17. test_client_elicitation_defaults — used by elicitation-sep1034-client-defaults scenario + addTool( + name = "test_client_elicitation_defaults", + description = "test_client_elicitation_defaults", + ) { + val schema = ElicitRequestParams.RequestedSchema( + properties = mapOf( + "name" to StringSchema(description = "User name", default = "John Doe"), + "age" to IntegerSchema(description = "User age", default = 30), + "score" to DoubleSchema(description = "User score", default = 95.5), + "status" to UntitledSingleSelectEnumSchema( + description = "User status", + enumValues = listOf("active", "inactive", "pending"), + default = "active", + ), + "verified" to BooleanSchema(description = "Verification status", default = true), + ), + required = emptyList(), + ) + val result = createElicitation( + "Please review and update the form fields with defaults", + schema, + ) + CallToolResult(listOf(TextContent("Elicitation completed: action=${result.action}, content=${result.content}"))) + } + + // 18. test-tool — simple tool used by auth scenarios + addTool( + name = "test-tool", + description = "Simple test tool for auth scenarios", + ) { + CallToolResult(listOf(TextContent("Test tool executed successfully"))) + } +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/InMemoryEventStore.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/InMemoryEventStore.kt new file mode 100644 index 0000000..ca7abb3 --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/InMemoryEventStore.kt @@ -0,0 +1,46 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance + +import io.modelcontextprotocol.kotlin.sdk.server.EventStore +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import java.util.concurrent.ConcurrentHashMap +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +@OptIn(ExperimentalUuidApi::class) +class InMemoryEventStore : EventStore { + + private val events = ConcurrentHashMap>() + private val streamEvents = ConcurrentHashMap>() + + override suspend fun storeEvent(streamId: String, message: JSONRPCMessage): String { + val eventId = "$streamId::${System.currentTimeMillis()}_${Uuid.random()}" + events[eventId] = message to streamId + streamEvents.getOrPut(streamId) { mutableListOf() }.add(eventId) + return eventId + } + + override suspend fun replayEventsAfter( + lastEventId: String, + sender: suspend (eventId: String, message: JSONRPCMessage) -> Unit, + ): String { + val streamId = getStreamIdForEventId(lastEventId) + ?: error("Unknown event ID: $lastEventId") + val eventIds = streamEvents[streamId] ?: return streamId + + var found = false + for (eventId in eventIds) { + if (!found) { + if (eventId == lastEventId) found = true + continue + } + val (message, _) = events[eventId] ?: continue + sender(eventId, message) + } + return streamId + } + + override suspend fun getStreamIdForEventId(eventId: String): String? { + val idx = eventId.indexOf("::") + return if (idx >= 0) eventId.substring(0, idx) else null + } +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/JWTScenario.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/JWTScenario.kt new file mode 100644 index 0000000..9a20681 --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/JWTScenario.kt @@ -0,0 +1,189 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance.auth + +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.sse.SSE +import io.ktor.client.request.forms.submitForm +import io.ktor.http.Parameters +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions +import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import java.security.KeyFactory +import java.security.Signature +import java.security.spec.PKCS8EncodedKeySpec +import java.util.Base64 +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +// Client Credentials JWT scenario +internal suspend fun runClientCredentialsJwt(serverUrl: String) { + val ctx = conformanceContext() + val clientId = ctx.requiredString("client_id") + val privateKeyPem = ctx.requiredString("private_key_pem") + val signingAlgorithm = ctx["signing_algorithm"]?.jsonPrimitive?.content ?: "ES256" + + val httpClient = HttpClient(CIO) { + install(SSE) + followRedirects = false + } + + httpClient.use { client -> + val resourceMetadata = discoverResourceMetadata(client, serverUrl) + val authServer = resourceMetadata["authorization_servers"]?.jsonArray?.firstOrNull()?.jsonPrimitive?.content + ?: error("No authorization_servers in resource metadata") + val oauthMetadata = fetchOAuthMetadata(client, authServer) + val tokenEndpoint = oauthMetadata["token_endpoint"]?.jsonPrimitive?.content + ?: error("No token_endpoint in AS metadata") + val issuer = oauthMetadata["issuer"]?.jsonPrimitive?.content + ?: error("No issuer in AS metadata") + + // Create JWT client assertion + val assertion = createJwtAssertion(clientId, issuer, privateKeyPem, signingAlgorithm) + + // Exchange for token + val tokenResponse = client.submitForm( + url = tokenEndpoint, + formParameters = Parameters.build { + append("grant_type", "client_credentials") + append("client_id", clientId) + append("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer") + append("client_assertion", assertion) + }, + ) + val accessToken = extractAccessToken(tokenResponse) + + withBearerToken(accessToken) { authedClient -> + val transport = StreamableHttpClientTransport(authedClient, serverUrl) + val client = Client( + clientInfo = Implementation("conformance-client-credentials-jwt", "1.0.0"), + options = ClientOptions(capabilities = ClientCapabilities()), + ) + client.connect(transport) + client.listTools() + client.close() + } + } +} + +// JWT Assertion +@OptIn(ExperimentalUuidApi::class) +private fun createJwtAssertion( + clientId: String, + audience: String, + privateKeyPem: String, + algorithm: String, +): String { + val header = buildJsonObject { + put("alg", algorithm) + put("typ", "JWT") + }.toString() + + val now = System.currentTimeMillis() / 1000 + val payload = buildJsonObject { + put("iss", clientId) + put("sub", clientId) + put("aud", audience) + put("iat", now) + put("exp", now + 300) + put("jti", Uuid.random().toString()) + }.toString() + + val headerB64 = Base64.getUrlEncoder().withoutPadding().encodeToString(header.toByteArray()) + val payloadB64 = Base64.getUrlEncoder().withoutPadding().encodeToString(payload.toByteArray()) + val signingInput = "$headerB64.$payloadB64" + + val signature = signJwt(signingInput, privateKeyPem, algorithm) + return "$signingInput.$signature" +} + +private fun signJwt(input: String, privateKeyPem: String, algorithm: String): String { + val pemBody = privateKeyPem + .replace("-----BEGIN PRIVATE KEY-----", "") + .replace("-----END PRIVATE KEY-----", "") + .replace("-----BEGIN EC PRIVATE KEY-----", "") + .replace("-----END EC PRIVATE KEY-----", "") + .replace("-----BEGIN RSA PRIVATE KEY-----", "") + .replace("-----END RSA PRIVATE KEY-----", "") + .replace("\n", "") + .replace("\r", "") + .trim() + + val keyBytes = Base64.getDecoder().decode(pemBody) + val keySpec = PKCS8EncodedKeySpec(keyBytes) + + val (keyAlgorithm, signatureAlgorithm) = when (algorithm) { + "ES256" -> "EC" to "SHA256withECDSA" + "RS256" -> "RSA" to "SHA256withRSA" + else -> error("Unsupported signing algorithm: $algorithm") + } + + val keyFactory = KeyFactory.getInstance(keyAlgorithm) + val privateKey = keyFactory.generatePrivate(keySpec) + + val sig = Signature.getInstance(signatureAlgorithm) + sig.initSign(privateKey) + sig.update(input.toByteArray()) + val rawSignature = sig.sign() + + // For EC, convert DER to raw r||s format for JWS + val signatureBytes = if (keyAlgorithm == "EC") { + derToRawEcSignature(rawSignature) + } else { + rawSignature + } + + return Base64.getUrlEncoder().withoutPadding().encodeToString(signatureBytes) +} + +private fun derToRawEcSignature(der: ByteArray): ByteArray { + // DER format: 0x30 len 0x02 rLen r 0x02 sLen s + require(der.size >= 2) { "DER signature too short" } + + var offset = 2 // skip SEQUENCE tag and length + if (der[1].toInt() and 0x80 != 0) { + offset += (der[1].toInt() and 0x7f) + } + + // Read r + require(offset < der.size) { "DER signature truncated before r tag" } + check(der[offset] == 0x02.toByte()) { "Expected INTEGER tag for r" } + offset++ + require(offset < der.size) { "DER signature truncated before r length" } + val rLen = der[offset].toInt() and 0xff + offset++ + require(offset + rLen <= der.size) { "DER signature truncated in r value" } + val r = der.copyOfRange(offset, offset + rLen) + offset += rLen + + // Read s + require(offset < der.size) { "DER signature truncated before s tag" } + check(der[offset] == 0x02.toByte()) { "Expected INTEGER tag for s" } + offset++ + require(offset < der.size) { "DER signature truncated before s length" } + val sLen = der[offset].toInt() and 0xff + offset++ + require(offset + sLen <= der.size) { "DER signature truncated in s value" } + val s = der.copyOfRange(offset, offset + sLen) + + // Each component should be 32 bytes for P-256 + val componentLen = 32 + val result = ByteArray(componentLen * 2) + + // Copy r (may need padding or trimming of leading zero) + val rStart = if (r.size > componentLen) r.size - componentLen else 0 + val rDest = if (r.size < componentLen) componentLen - r.size else 0 + r.copyInto(result, rDest, rStart, r.size) + + // Copy s + val sStart = if (s.size > componentLen) s.size - componentLen else 0 + val sDest = componentLen + if (s.size < componentLen) componentLen - s.size else 0 + s.copyInto(result, sDest, sStart, s.size) + + return result +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/authCodeFlow.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/authCodeFlow.kt new file mode 100644 index 0000000..c59e8e0 --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/authCodeFlow.kt @@ -0,0 +1,154 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance.auth + +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.HttpSend +import io.ktor.client.plugins.plugin +import io.ktor.client.plugins.sse.SSE +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions +import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonPrimitive +import java.util.UUID + +internal suspend fun runAuthClient(serverUrl: String) { + val httpClient = HttpClient(CIO) { + install(SSE) + followRedirects = false + } + + var accessToken: String? = null + var authAttempts = 0 + // Cache discovery and credentials across retries + var cachedDiscovery: DiscoveryResult? = null + var cachedCredentials: ClientCredentials? = null + + httpClient.plugin(HttpSend).intercept { request -> + // Add existing token if available + if (accessToken != null) { + request.headers.remove(HttpHeaders.Authorization) + request.headers.append(HttpHeaders.Authorization, "Bearer $accessToken") + } + + val response = execute(request) + val status = response.response.status + + // Determine if we need to (re-)authorize + val needsAuth = status == HttpStatusCode.Unauthorized + val wwwAuth = response.response.headers[HttpHeaders.WWWAuthenticate] ?: "" + val stepUpScope = if (status == HttpStatusCode.Forbidden) parseStepUpScope(wwwAuth) else null + val needsStepUp = stepUpScope != null + + if ((needsAuth || needsStepUp) && authAttempts < 3) { + authAttempts++ + + // Discover metadata (cache across retries) + if (cachedDiscovery == null) { + val resourceMetadataUrl = extractParam(wwwAuth, "resource_metadata") + cachedDiscovery = discoverOAuthMetadata(httpClient, serverUrl, resourceMetadataUrl) + } + val discovery: DiscoveryResult = cachedDiscovery + + // Validate PRM resource matches server URL (RFC 8707) + val discoveredResource = discovery.resourceUrl + if (discoveredResource != null) { + val normalizedResource = discoveredResource.trimEnd('/') + val normalizedServerUrl = serverUrl.trimEnd('/') + val matches = normalizedServerUrl == normalizedResource || + normalizedServerUrl.startsWith("$normalizedResource/") + require(matches) { + "PRM resource mismatch: resource='$discoveredResource' does not match server URL='$serverUrl'" + } + } + + val metadata = discovery.asMetadata + + val authEndpoint = metadata["authorization_endpoint"]?.jsonPrimitive?.content + ?: error("No authorization_endpoint in metadata") + val tokenEndpoint = metadata["token_endpoint"]?.jsonPrimitive?.content + ?: error("No token_endpoint in metadata") + + val tokenEndpointAuthMethods = metadata["token_endpoint_auth_methods_supported"] + ?.jsonArray?.map { it.jsonPrimitive.content } + ?: listOf("client_secret_post") + val tokenAuthMethod = tokenEndpointAuthMethods.firstOrNull() ?: "client_secret_post" + + // Verify PKCE support + verifyPkceSupport(metadata) + + // Resolve client credentials (cache across retries) + if (cachedCredentials == null) { + cachedCredentials = resolveClientCredentials(httpClient, metadata) + } + val creds: ClientCredentials = cachedCredentials + + // Determine scope + val scope = if (needsStepUp) { + stepUpScope + } else { + val wwwAuthScope = extractParam(wwwAuth, "scope") + selectScope(wwwAuthScope, discovery.scopesSupported) + } + + // PKCE + val codeVerifier = generateCodeVerifier() + val codeChallenge = generateCodeChallenge(codeVerifier) + + // CSRF state parameter + val state = UUID.randomUUID().toString() + + // Build authorization URL + val authUrl = buildAuthorizationUrl( + authEndpoint, + creds.clientId, + CALLBACK_URL, + codeChallenge, + scope, + discovery.resourceUrl, + state, + ) + + // Follow the authorization redirect to get auth code + val authCode = followAuthorizationRedirect(httpClient, authUrl, CALLBACK_URL, state) + + // Exchange code for tokens + accessToken = exchangeCodeForTokens( + httpClient, + tokenEndpoint, + authCode, + creds.clientId, + creds.clientSecret, + CALLBACK_URL, + codeVerifier, + tokenAuthMethod, + discovery.resourceUrl, + ) + + // Retry the original request with the token + request.headers.remove(HttpHeaders.Authorization) + request.headers.append(HttpHeaders.Authorization, "Bearer $accessToken") + execute(request) + } else { + response + } + } + + httpClient.use { client -> + val transport = StreamableHttpClientTransport(client, serverUrl) + val mcpClient = Client( + clientInfo = Implementation("test-auth-client", "1.0.0"), + options = ClientOptions(capabilities = ClientCapabilities()), + ) + mcpClient.connect(transport) + mcpClient.listTools() + mcpClient.callTool(CallToolRequest(CallToolRequestParams(name = "test-tool"))) + mcpClient.close() + } +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/basicScenario.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/basicScenario.kt new file mode 100644 index 0000000..a0d6d86 --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/basicScenario.kt @@ -0,0 +1,54 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance.auth + +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.sse.SSE +import io.ktor.client.request.forms.submitForm +import io.ktor.client.request.header +import io.ktor.http.HttpHeaders +import io.ktor.http.Parameters +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions +import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import java.util.Base64 + +// Client Credentials Basic scenario +internal suspend fun runClientCredentialsBasic(serverUrl: String) { + val ctx = conformanceContext() + val clientId = ctx.requiredString("client_id") + val clientSecret = ctx.requiredString("client_secret") + + val httpClient = HttpClient(CIO) { + install(SSE) + followRedirects = false + } + + httpClient.use { client -> + val tokenEndpoint = discoverTokenEndpoint(client, serverUrl) + + // Exchange credentials for token using Basic auth + val basicAuth = Base64.getEncoder().encodeToString("$clientId:$clientSecret".toByteArray()) + val tokenResponse = client.submitForm( + url = tokenEndpoint, + formParameters = Parameters.build { + append("grant_type", "client_credentials") + }, + ) { + header(HttpHeaders.Authorization, "Basic $basicAuth") + } + val accessToken = extractAccessToken(tokenResponse) + + withBearerToken(accessToken) { authedClient -> + val transport = StreamableHttpClientTransport(authedClient, serverUrl) + val client = Client( + clientInfo = Implementation("conformance-client-credentials-basic", "1.0.0"), + options = ClientOptions(capabilities = ClientCapabilities()), + ) + client.connect(transport) + client.listTools() + client.close() + } + } +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/clientRegistration.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/clientRegistration.kt new file mode 100644 index 0000000..8e84a15 --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/clientRegistration.kt @@ -0,0 +1,73 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance.auth + +import io.ktor.client.HttpClient +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.client.statement.bodyAsText +import io.ktor.http.ContentType +import io.ktor.http.contentType +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put + +internal data class ClientCredentials(val clientId: String, val clientSecret: String?) + +/** + * Resolve client credentials per spec priority: + * 1. Pre-registered (from MCP_CONFORMANCE_CONTEXT) + * 2. CIMD (client_id_metadata_document_supported) + * 3. Dynamic registration (registration_endpoint) + * 4. Error + */ +internal suspend fun resolveClientCredentials(httpClient: HttpClient, asMetadata: JsonObject): ClientCredentials { + // 1. Pre-registered + val contextJson = System.getenv("MCP_CONFORMANCE_CONTEXT") + if (contextJson != null) { + val ctx = json.parseToJsonElement(contextJson).jsonObject + val clientId = ctx["client_id"]?.jsonPrimitive?.content + if (clientId != null) { + val clientSecret = ctx["client_secret"]?.jsonPrimitive?.content + return ClientCredentials(clientId, clientSecret) + } + } + + // 2. CIMD + val cimdSupported = asMetadata["client_id_metadata_document_supported"] + ?.jsonPrimitive?.content?.toBoolean() ?: false + if (cimdSupported) { + return ClientCredentials(CIMD_CLIENT_METADATA_URL, null) + } + + // 3. Dynamic registration + val registrationEndpoint = asMetadata["registration_endpoint"]?.jsonPrimitive?.content + if (registrationEndpoint != null) { + return dynamicClientRegistration(httpClient, registrationEndpoint) + } + + error("No way to register client: no pre-registered credentials, CIMD not supported, and no registration_endpoint") +} + +private suspend fun dynamicClientRegistration( + httpClient: HttpClient, + registrationEndpoint: String, +): ClientCredentials { + val regBody = buildJsonObject { + put("client_name", "test-auth-client") + put("redirect_uris", buildJsonArray { add(kotlinx.serialization.json.JsonPrimitive(CALLBACK_URL)) }) + put("grant_types", buildJsonArray { add(kotlinx.serialization.json.JsonPrimitive("authorization_code")) }) + put("response_types", buildJsonArray { add(kotlinx.serialization.json.JsonPrimitive("code")) }) + put("token_endpoint_auth_method", "client_secret_post") + } + + val response = httpClient.post(registrationEndpoint) { + contentType(ContentType.Application.Json) + setBody(regBody.toString()) + } + val regJson = json.parseToJsonElement(response.bodyAsText()).jsonObject + val clientId = regJson["client_id"]?.jsonPrimitive?.content ?: error("No client_id in registration response") + val clientSecret = regJson["client_secret"]?.jsonPrimitive?.content + return ClientCredentials(clientId, clientSecret) +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/crossAppAccessScenario.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/crossAppAccessScenario.kt new file mode 100644 index 0000000..db4b7f0 --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/crossAppAccessScenario.kt @@ -0,0 +1,82 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance.auth + +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.sse.SSE +import io.ktor.client.request.forms.submitForm +import io.ktor.client.request.header +import io.ktor.http.HttpHeaders +import io.ktor.http.Parameters +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions +import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonPrimitive +import java.util.Base64 + +// SEP-990 Enterprise Managed OAuth: Cross-App Access complete flow +internal suspend fun runCrossAppAccess(serverUrl: String) { + val ctx = conformanceContext() + val clientId = ctx.requiredString("client_id") + val clientSecret = ctx.requiredString("client_secret") + val idpIdToken = ctx.requiredString("idp_id_token") + val idpTokenEndpoint = ctx.requiredString("idp_token_endpoint") + + val httpClient = HttpClient(CIO) { + install(SSE) + followRedirects = false + } + + httpClient.use { client -> + // Discover PRM + AS metadata + val resourceMeta = discoverResourceMetadata(client, serverUrl) + val resourceUrl = resourceMeta["resource"]?.jsonPrimitive?.content + ?: error("No resource in resource metadata") + val authServer = resourceMeta["authorization_servers"]?.jsonArray?.firstOrNull()?.jsonPrimitive?.content + ?: error("No authorization_servers in resource metadata") + val asMeta = fetchOAuthMetadata(client, authServer) + val tokenEndpoint = asMeta["token_endpoint"]?.jsonPrimitive?.content + ?: error("No token_endpoint in AS metadata") + + // RFC 8693 Token Exchange at IDP: exchange ID token for ID-JAG + val idpResponse = client.submitForm( + url = idpTokenEndpoint, + formParameters = Parameters.build { + append("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange") + append("subject_token", idpIdToken) + append("subject_token_type", "urn:ietf:params:oauth:token-type:id_token") + append("requested_token_type", "urn:ietf:params:oauth:token-type:id-jag") + append("audience", authServer) + append("resource", resourceUrl) + }, + ) + val idJag = extractAccessToken(idpResponse) + + // RFC 7523 JWT Bearer Grant at AS with Basic auth + val basicAuth = Base64.getEncoder().encodeToString("$clientId:$clientSecret".toByteArray()) + val asResponse = client.submitForm( + url = tokenEndpoint, + formParameters = Parameters.build { + append("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer") + append("assertion", idJag) + }, + ) { + header(HttpHeaders.Authorization, "Basic $basicAuth") + } + val accessToken = extractAccessToken(asResponse) + + // Use access token for MCP requests + withBearerToken(accessToken) { authedClient -> + val transport = StreamableHttpClientTransport(authedClient, serverUrl) + val mcpClient = Client( + clientInfo = Implementation("conformance-cross-app-access", "1.0.0"), + options = ClientOptions(capabilities = ClientCapabilities()), + ) + mcpClient.connect(transport) + mcpClient.listTools() + mcpClient.close() + } + } +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/discovery.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/discovery.kt new file mode 100644 index 0000000..73979db --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/discovery.kt @@ -0,0 +1,48 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance.auth + +import io.ktor.client.HttpClient +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText +import io.ktor.http.isSuccess +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + +internal data class DiscoveryResult( + val asMetadata: JsonObject, + val resourceUrl: String?, + val scopesSupported: List?, +) + +internal suspend fun discoverOAuthMetadata( + httpClient: HttpClient, + serverUrl: String, + resourceMetadataUrl: String?, +): DiscoveryResult { + // Get resource metadata + val resourceMeta = if (resourceMetadataUrl != null) { + val resp = httpClient.get(resourceMetadataUrl) + if (!resp.status.isSuccess()) { + error("Failed to fetch resource metadata from $resourceMetadataUrl: ${resp.status}") + } + json.parseToJsonElement(resp.bodyAsText()).jsonObject + } else { + discoverResourceMetadata(httpClient, serverUrl) + } + + val resourceUrl = resourceMeta["resource"]?.jsonPrimitive?.content + val scopesSupported = resourceMeta["scopes_supported"] + ?.jsonArray?.map { it.jsonPrimitive.content } + val authServer = resourceMeta["authorization_servers"]?.jsonArray?.firstOrNull()?.jsonPrimitive?.content + + val oauthMeta = if (authServer != null) { + fetchOAuthMetadata(httpClient, authServer) + } else { + // Fallback: try well-known on server URL origin + val origin = extractOrigin(serverUrl) + fetchOAuthMetadata(httpClient, origin) + } + + return DiscoveryResult(oauthMeta, resourceUrl, scopesSupported) +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/pkce.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/pkce.kt new file mode 100644 index 0000000..e1c5419 --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/pkce.kt @@ -0,0 +1,30 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance.auth + +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonPrimitive +import java.security.MessageDigest +import java.util.Base64 + +internal fun generateCodeVerifier(): String { + val bytes = ByteArray(32) + java.security.SecureRandom().nextBytes(bytes) + return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes) +} + +internal fun generateCodeChallenge(verifier: String): String { + val digest = MessageDigest.getInstance("SHA-256").digest(verifier.toByteArray(Charsets.US_ASCII)) + return Base64.getUrlEncoder().withoutPadding().encodeToString(digest) +} + +/** + * Verify that the AS metadata advertises S256 in code_challenge_methods_supported. + * Abort if PKCE S256 is not supported. + */ +internal fun verifyPkceSupport(asMetadata: JsonObject) { + val methods = asMetadata["code_challenge_methods_supported"] + ?.jsonArray?.map { it.jsonPrimitive.content } + require(methods != null && "S256" in methods) { + "Authorization server does not support PKCE S256 (code_challenge_methods_supported: $methods)" + } +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/registration.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/registration.kt new file mode 100644 index 0000000..4964f20 --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/registration.kt @@ -0,0 +1,32 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance.auth + +import io.modelcontextprotocol.kotlin.sdk.conformance.scenarioHandlers + +// Registration +fun registerAuthScenarios() { + val authScenarios = listOf( + "auth/metadata-default", + "auth/metadata-var1", + "auth/metadata-var2", + "auth/metadata-var3", + "auth/basic-cimd", + "auth/scope-from-www-authenticate", + "auth/scope-from-scopes-supported", + "auth/scope-omitted-when-undefined", + "auth/scope-step-up", + "auth/scope-retry-limit", + "auth/token-endpoint-auth-basic", + "auth/token-endpoint-auth-post", + "auth/token-endpoint-auth-none", + "auth/resource-mismatch", + "auth/pre-registration", + "auth/2025-03-26-oauth-metadata-backcompat", + "auth/2025-03-26-oauth-endpoint-fallback", + ) + for (name in authScenarios) { + scenarioHandlers[name] = ::runAuthClient + } + scenarioHandlers["auth/client-credentials-jwt"] = ::runClientCredentialsJwt + scenarioHandlers["auth/client-credentials-basic"] = ::runClientCredentialsBasic + scenarioHandlers["auth/cross-app-access-complete-flow"] = ::runCrossAppAccess +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/scopeHandling.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/scopeHandling.kt new file mode 100644 index 0000000..33b69ce --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/scopeHandling.kt @@ -0,0 +1,29 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance.auth + +internal fun extractParam(wwwAuth: String, param: String): String? { + val regex = Regex("""$param="([^"]+)"""") + return regex.find(wwwAuth)?.groupValues?.get(1) +} + +/** + * Select scope per MCP spec priority: + * 1. scope from WWW-Authenticate header + * 2. scopes_supported from Protected Resource Metadata (space-joined) + * 3. null (omit scope entirely) + */ +internal fun selectScope(wwwAuthScope: String?, scopesSupported: List?): String? { + if (wwwAuthScope != null) return wwwAuthScope + if (!scopesSupported.isNullOrEmpty()) return scopesSupported.joinToString(" ") + return null +} + +/** + * Detect 403 with error="insufficient_scope" and extract the new scope. + * Returns the scope string if step-up is needed, null otherwise. + */ +internal fun parseStepUpScope(wwwAuth: String?): String? { + if (wwwAuth == null) return null + val error = extractParam(wwwAuth, "error") + if (error != "insufficient_scope") return null + return extractParam(wwwAuth, "scope") +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/tokenExchange.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/tokenExchange.kt new file mode 100644 index 0000000..0c79bb9 --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/tokenExchange.kt @@ -0,0 +1,174 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance.auth + +import io.ktor.client.HttpClient +import io.ktor.client.request.forms.submitForm +import io.ktor.client.request.get +import io.ktor.client.request.header +import io.ktor.client.statement.bodyAsText +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.Parameters +import io.ktor.http.isSuccess +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import java.net.URI +import java.net.URLEncoder +import java.util.Base64 + +internal fun buildAuthorizationUrl( + authEndpoint: String, + clientId: String, + redirectUri: String, + codeChallenge: String, + scope: String?, + resource: String?, + state: String, +): String { + val params = buildString { + append("response_type=code") + append("&client_id=${URLEncoder.encode(clientId, "UTF-8")}") + append("&redirect_uri=${URLEncoder.encode(redirectUri, "UTF-8")}") + append("&code_challenge=${URLEncoder.encode(codeChallenge, "UTF-8")}") + append("&code_challenge_method=S256") + append("&state=${URLEncoder.encode(state, "UTF-8")}") + if (scope != null) { + append("&scope=${URLEncoder.encode(scope, "UTF-8")}") + } + if (resource != null) { + append("&resource=${URLEncoder.encode(resource, "UTF-8")}") + } + } + return if (authEndpoint.contains("?")) "$authEndpoint&$params" else "$authEndpoint?$params" +} + +internal suspend fun followAuthorizationRedirect( + httpClient: HttpClient, + authUrl: String, + expectedCallbackUrl: String, + expectedState: String, +): String { + val response = httpClient.get(authUrl) + + if (response.status == HttpStatusCode.Found || + response.status == HttpStatusCode.MovedPermanently || + response.status == HttpStatusCode.TemporaryRedirect || + response.status == HttpStatusCode.SeeOther + ) { + val location = response.headers[HttpHeaders.Location] + ?: error("No Location header in redirect response") + + require(location.startsWith(expectedCallbackUrl)) { + "Redirect location does not match expected callback URL" + } + + val uri = URI(location) + val queryParams = uri.query?.split("&")?.mapNotNull { + val parts = it.split("=", limit = 2) + if (parts.size == 2) parts[0] to java.net.URLDecoder.decode(parts[1], "UTF-8") else null + }?.toMap() ?: emptyMap() + + val returnedState = queryParams["state"] + require(returnedState == expectedState) { + "State parameter mismatch in authorization redirect" + } + + return queryParams["code"] ?: error("No authorization code in redirect response") + } + + error("Expected redirect from auth endpoint, got ${response.status}") +} + +internal suspend fun exchangeCodeForTokens( + httpClient: HttpClient, + tokenEndpoint: String, + code: String, + clientId: String, + clientSecret: String?, + redirectUri: String, + codeVerifier: String, + tokenAuthMethod: String, + resource: String?, +): String { + val response = when (tokenAuthMethod) { + "client_secret_basic" -> { + val basicAuth = Base64.getEncoder() + .encodeToString("$clientId:${clientSecret ?: ""}".toByteArray()) + httpClient.submitForm( + url = tokenEndpoint, + formParameters = Parameters.build { + append("grant_type", "authorization_code") + append("code", code) + append("redirect_uri", redirectUri) + append("code_verifier", codeVerifier) + if (resource != null) { + append("resource", resource) + } + }, + ) { + header(HttpHeaders.Authorization, "Basic $basicAuth") + } + } + + "none" -> { + httpClient.submitForm( + url = tokenEndpoint, + formParameters = Parameters.build { + append("grant_type", "authorization_code") + append("code", code) + append("client_id", clientId) + append("redirect_uri", redirectUri) + append("code_verifier", codeVerifier) + if (resource != null) { + append("resource", resource) + } + }, + ) + } + + else -> { + // client_secret_post (default) + httpClient.submitForm( + url = tokenEndpoint, + formParameters = Parameters.build { + append("grant_type", "authorization_code") + append("code", code) + append("client_id", clientId) + if (clientSecret != null) { + append("client_secret", clientSecret) + } + append("redirect_uri", redirectUri) + append("code_verifier", codeVerifier) + if (resource != null) { + append("resource", resource) + } + }, + ) + } + } + + // Check HTTP status + if (!response.status.isSuccess()) { + val body = response.bodyAsText() + val errorDetail = try { + val obj = json.parseToJsonElement(body).jsonObject + val err = obj["error"]?.jsonPrimitive?.content ?: "unknown" + val desc = obj["error_description"]?.jsonPrimitive?.content + if (desc != null) "$err: $desc" else err + } catch (_: Exception) { + body + } + error("Token exchange failed (${response.status}): $errorDetail") + } + + val tokenJson = json.parseToJsonElement(response.bodyAsText()).jsonObject + + // Check for error field in response body (some servers return 200 with error) + val errorField = tokenJson["error"]?.jsonPrimitive?.content + if (errorField != null) { + val desc = tokenJson["error_description"]?.jsonPrimitive?.content + error("Token exchange error: $errorField${if (desc != null) " - $desc" else ""}") + } + + return tokenJson["access_token"]?.jsonPrimitive?.content + ?: error("No access_token in token response") +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/utils.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/utils.kt new file mode 100644 index 0000000..4bc75a0 --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/utils.kt @@ -0,0 +1,120 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance.auth + +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.HttpSend +import io.ktor.client.plugins.plugin +import io.ktor.client.plugins.sse.SSE +import io.ktor.client.request.get +import io.ktor.client.statement.HttpResponse +import io.ktor.client.statement.bodyAsText +import io.ktor.http.HttpHeaders +import io.ktor.http.isSuccess +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import java.net.URI +import kotlin.text.ifEmpty + +internal val json = Json { ignoreUnknownKeys = true } + +internal fun conformanceContext(): JsonObject { + val contextJson = System.getenv("MCP_CONFORMANCE_CONTEXT") + ?: error("MCP_CONFORMANCE_CONTEXT not set") + return json.parseToJsonElement(contextJson).jsonObject +} + +internal fun JsonObject.requiredString(key: String): String = this[key]?.jsonPrimitive?.content ?: error("Missing $key") + +internal const val CIMD_CLIENT_METADATA_URL = "https://conformance-test.local/client-metadata.json" +internal const val CALLBACK_URL = "http://localhost:3000/callback" + +internal fun extractOrigin(url: String): String { + val uri = URI(url) + return "${uri.scheme}://${uri.host}${if (uri.port > 0) ":${uri.port}" else ""}" +} + +internal suspend fun discoverResourceMetadata(httpClient: HttpClient, serverUrl: String): JsonObject { + val origin = extractOrigin(serverUrl) + val path = URI(serverUrl).path.ifEmpty { "/" } + + // Try RFC 9728 format first: /.well-known/oauth-protected-resource/ + val wellKnownUrl = "$origin/.well-known/oauth-protected-resource$path" + val response = httpClient.get(wellKnownUrl) + if (response.status.isSuccess()) { + return json.parseToJsonElement(response.bodyAsText()).jsonObject + } + + // Fallback: try root + val fallbackUrl = "$origin/.well-known/oauth-protected-resource" + val fallbackResponse = httpClient.get(fallbackUrl) + if (!fallbackResponse.status.isSuccess()) { + error( + "Failed to discover resource metadata at $wellKnownUrl (${response.status}) and $fallbackUrl (${fallbackResponse.status})", + ) + } + return json.parseToJsonElement(fallbackResponse.bodyAsText()).jsonObject +} + +internal suspend fun fetchOAuthMetadata(httpClient: HttpClient, authServerUrl: String): JsonObject { + val origin = extractOrigin(authServerUrl) + val path = URI(authServerUrl).path.ifEmpty { "/" } + + // RFC 8414 §3: /.well-known/oauth-authorization-server/ + val oauthUrl = "$origin/.well-known/oauth-authorization-server$path" + val oauthResponse = httpClient.get(oauthUrl) + if (oauthResponse.status.isSuccess()) { + return json.parseToJsonElement(oauthResponse.bodyAsText()).jsonObject + } + + // OIDC Discovery with path insertion: /.well-known/openid-configuration/ + val oidcPathUrl = "$origin/.well-known/openid-configuration$path" + val oidcPathResponse = httpClient.get(oidcPathUrl) + if (oidcPathResponse.status.isSuccess()) { + return json.parseToJsonElement(oidcPathResponse.bodyAsText()).jsonObject + } + + // Fallback: OpenID Connect discovery (issuer + /.well-known/openid-configuration) + val oidcUrl = "$authServerUrl/.well-known/openid-configuration" + val oidcResponse = httpClient.get(oidcUrl) + if (oidcResponse.status.isSuccess()) { + return json.parseToJsonElement(oidcResponse.bodyAsText()).jsonObject + } + + error( + "Failed to fetch OAuth metadata from $oauthUrl (${oauthResponse.status}) and $oidcUrl (${oidcResponse.status})", + ) +} + +internal suspend fun discoverTokenEndpoint(httpClient: HttpClient, serverUrl: String): String { + val resourceMetadata = discoverResourceMetadata(httpClient, serverUrl) + val authServer = resourceMetadata["authorization_servers"]?.jsonArray?.firstOrNull()?.jsonPrimitive?.content + ?: error("No authorization_servers in resource metadata") + + val oauthMetadata = fetchOAuthMetadata(httpClient, authServer) + return oauthMetadata["token_endpoint"]?.jsonPrimitive?.content + ?: error("No token_endpoint") +} + +internal suspend fun extractAccessToken(tokenResponse: HttpResponse): String { + if (!tokenResponse.status.isSuccess()) { + error("Token request failed: ${tokenResponse.status}") + } + val tokenJson = json.parseToJsonElement(tokenResponse.bodyAsText()).jsonObject + return tokenJson["access_token"]?.jsonPrimitive?.content + ?: error("No access_token in token response") +} + +internal suspend fun withBearerToken(accessToken: String, block: suspend (HttpClient) -> T): T { + val client = HttpClient(CIO) { + install(SSE) + } + client.plugin(HttpSend).intercept { request -> + request.headers.remove(HttpHeaders.Authorization) + request.headers.append(HttpHeaders.Authorization, "Bearer $accessToken") + execute(request) + } + return client.use { block(it) } +} diff --git a/conformance-test/src/main/resources/simplelogger.properties b/conformance-test/src/main/resources/simplelogger.properties new file mode 100644 index 0000000..16141d3 --- /dev/null +++ b/conformance-test/src/main/resources/simplelogger.properties @@ -0,0 +1,9 @@ +# Level of logging for the ROOT logger: ERROR, WARN, INFO, DEBUG, TRACE (default is INFO) +org.slf4j.simpleLogger.defaultLogLevel=INFO +org.slf4j.simpleLogger.showThreadName=true +org.slf4j.simpleLogger.showDateTime=false + +# Log level for specific packages or classes +org.slf4j.simpleLogger.log.io.ktor.server=DEBUG +org.slf4j.simpleLogger.log.io.modelcontextprotocol=DEBUG +org.slf4j.simpleLogger.log.io.modelcontextprotocol.kotlin.sdk.conformance=INFO diff --git a/context7.json b/context7.json new file mode 100644 index 0000000..5fde250 --- /dev/null +++ b/context7.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://context7.com/schema/context7.json", + "projectTitle": "Model Context Protocol Kotlin SDK", + "description": "Kotlin Multiplatform SDK for MCP — client and server implementations", + "excludeFolders": [ + ".github", + "buildSrc", + "config", + "gradle/wrapper" + ], + "excludeFiles": [ + "CONTRIBUTING.md", + "LICENSE", + "SECURITY.md", + "gradlew", + "gradlew.bat", + ".editorconfig", + "knit.properties", + "gradle.properties", + "settings.gradle.kts", + "conformance-baseline.yml", + "run-conformance.sh", + "detekt-baseline.xml", + "detekt-baseline-test.xml", + "detekt-baseline-main.xml", + "detekt-baseline-commonMainSourceSet.xml", + "detekt-baseline-commonTestSourceSet.xml" + ] +} \ No newline at end of file diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..8eba6c8 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +src/ diff --git a/docs/build.gradle.kts b/docs/build.gradle.kts new file mode 100644 index 0000000..1857ef1 --- /dev/null +++ b/docs/build.gradle.kts @@ -0,0 +1,37 @@ +plugins { + kotlin("jvm") + alias(libs.plugins.knit) +} + +dependencies { + implementation(project(":kotlin-sdk")) + implementation(libs.ktor.server.cio) + implementation(libs.ktor.serialization) + implementation(libs.ktor.server.content.negotiation) +} + +tasks.matching { + it.name == "ktlintMainSourceSetCheck" || it.name == "ktlintMainSourceSetFormat" +}.configureEach { + enabled = false +} + +knit { + rootDir = project.rootDir + files = files(project.rootDir.resolve("README.md")) + defaultLineSeparator = "\n" + siteRoot = "" // Disable site root validation +} + +// Only run knitCheck when explicitly requested, not as part of build/check +tasks.named("knitCheck") { + onlyIf { + gradle.startParameter.taskNames.any { + it.contains("knitCheck") || it.contains("knit") + } + } +} + +tasks.clean { + delete("src") +} diff --git a/docs/moduledoc.md b/docs/moduledoc.md index 508fef9..05ce370 100644 --- a/docs/moduledoc.md +++ b/docs/moduledoc.md @@ -1,21 +1,17 @@ # MCP Kotlin SDK -Kotlin SDK for the Model Context Protocol (MCP). -This is a Kotlin Multiplatform library that helps you build MCP clients and servers that speak the same protocol and -share the same types. -The SDK focuses on clarity, small building blocks, and first‑class coroutine support. +Kotlin Multiplatform implementation of the Model Context Protocol (MCP). The SDK focuses on clear, explicit APIs, +small building blocks, and first-class coroutine support so clients and servers share the same well-typed messages and +transports. -Use the umbrella `kotlin-sdk` artifact when you want a single dependency that brings the core types plus both client and -server toolkits. If you only need one side, depend on `kotlin-sdk-client` or `kotlin-sdk-server` directly. - -Gradle (Kotlin DSL): +Use the umbrella `kotlin-sdk` artifact to bring in everything at once, or depend on the focused modules directly: ```kotlin dependencies { - // Convenience bundle with everything you need to start + // All-in-one bundle implementation("io.modelcontextprotocol:kotlin-sdk:") - // Or pick modules explicitly + // Or pick sides explicitly implementation("io.modelcontextprotocol:kotlin-sdk-client:") implementation("io.modelcontextprotocol:kotlin-sdk-server:") } @@ -25,79 +21,74 @@ dependencies { ## Module kotlin-sdk-core -Foundational, platform‑agnostic pieces: +Foundation shared by both sides: -- Protocol data model and JSON serialization (kotlinx.serialization) -- Request/response and notification types used by both sides of MCP -- Coroutine‑friendly protocol engine and utilities -- Transport abstractions shared by client and server +- Protocol data model for MCP requests, results, notifications, capabilities, and content types. +- `McpJson` (kotlinx.serialization) with MCP-friendly defaults plus helpers for converting native values to JSON. +- Transport abstractions (`Transport`, `AbstractTransport`, `WebSocketMcpTransport`) and streaming `ReadBuffer`. +- `Protocol` base class that handles JSON-RPC framing, correlation, progress tokens, and capability assertions. -You typically do not use `core` directly in application code; it is pulled in by the client/server modules. Use it -explicitly if you only need the protocol types or plan to implement a custom transport. +Use `core` when you need the raw types or want to author a custom transport. Public APIs are explicit to keep the shared +surface stable across platforms. --- ## Module kotlin-sdk-client -High‑level client API for connecting to an MCP server and invoking its tools, prompts, and resources. Ships with several -transports: +High-level client for connecting to MCP servers and invoking their features: -- WebSocketClientTransport – low latency, full‑duplex -- SSEClientTransport – Server‑Sent Events over HTTP -- StdioClientTransport – CLI‑friendly stdio bridge -- StreamableHttpClientTransport – simple HTTP streaming +- `Client` runtime (or `mcpClient` helper) performs the MCP handshake and exposes `serverCapabilities`, + `serverVersion`, and `serverInstructions`. +- Typed operations for tools, prompts, resources, completion, logging, roots, sampling, and elicitation with capability + enforcement. +- Transports: `StdioClientTransport`, `SseClientTransport`, `WebSocketClientTransport`, and `StreamableHttpClientTransport`, + plus Ktor client extensions for quick wiring. -A minimal client: +Minimal WebSocket client: ```kotlin -val client = Client( - clientInfo = Implementation(name = "sample-client", version = "1.0.0") +val client = mcpClient( + clientInfo = Implementation("sample-client", "1.0.0"), + clientOptions = ClientOptions(ClientCapabilities(tools = ClientCapabilities.Tools())), + transport = WebSocketClientTransport("ws://localhost:8080/mcp") ) -client.connect(WebSocketClientTransport("ws://localhost:8080/mcp")) - val tools = client.listTools() -val result = client.callTool( - name = "echo", - arguments = mapOf("text" to "Hello, MCP!") -) +val result = client.callTool("echo", mapOf("text" to "Hello, MCP!")) +println(result.content) ``` --- ## Module kotlin-sdk-server -Lightweight server toolkit for hosting MCP tools, prompts, and resources. It provides a small, composable API and -ready‑to‑use transports: +Server toolkit for exposing MCP tools, prompts, and resources: -- StdioServerTransport – integrates well with CLIs and editors -- SSE/WebSocket helpers for Ktor – easy HTTP deployment +- `Server` runtime coordinates sessions, initialization flow, and capability enforcement with registries for tools, + prompts, resources, and templates. +- Transports: `StdioServerTransport` for CLI/editor bridges; Ktor extensions (`mcp` for SSE + POST back-channel and + `mcpWebSocket` for WebSocket) for HTTP hosting. +- Built-in notifications for list changes and resource subscriptions when capabilities enable them. -Register tools and run over stdio: +Minimal Ktor SSE server: ```kotlin - -val server = Server( - serverInfo = Implementation(name = "sample-server", version = "1.0.0"), - options = ServerOptions(ServerCapabilities()) -) - -server.addTool( - name = "echo", - description = "Echoes the provided text" -) { request -> - // Build and return a CallToolResult from request.arguments - // (see CallToolResult and related types in kotlin-sdk-core) - /* ... */ +fun Application.module() { + mcp { + Server( + serverInfo = Implementation("sample-server", "1.0.0"), + options = ServerOptions(ServerCapabilities( + tools = ServerCapabilities.Tools(listChanged = true), + resources = ServerCapabilities.Resources(listChanged = true, subscribe = true), + )), + ) { + addTool(name = "echo", description = "Echo text back") { request -> + val text = request.params.arguments?.get("text")?.jsonPrimitive?.content ?: "" + CallToolResult(content = listOf(TextContent("You said: $text"))) + } + } + } } - -// Bridge the protocol over stdio -val transport = StdioServerTransport( - inputStream = kotlinx.io.files.Path("/dev/stdin").source(), - outputStream = kotlinx.io.files.Path("/dev/stdout").sink() -) -// Start transport and wire it with the server using provided helpers in the SDK. ``` -For HTTP deployments, use the Ktor extensions included in the module to expose an MCP WebSocket or SSE endpoint with a -few lines of code. +Pick the module that matches your role, or use the umbrella artifact to get both sides with the shared core. diff --git a/gradle.properties b/gradle.properties index 7e63634..a5655c3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,14 +1,11 @@ org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8 org.gradle.parallel=true org.gradle.caching=true -# Dokka -org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled -org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true # Kotlin kotlin.code.style=official kotlin.daemon.jvmargs=-Xmx4G -# Build JS targets using npm package manager https://kotlinlang.org/docs/js-project-setup.html#npm-dependencies -kotlin.js.yarn=false # MPP -kotlin.mpp.enableCInteropCommonization=true kotlin.native.ignoreDisabledTargets=true + +group=io.modelcontextprotocol +version=0.15.0 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2e884a6..a8d034f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,75 +1,80 @@ [versions] # plugins version -kotlin = "2.2.10" -dokka = "2.0.0" -atomicfu = "0.29.0" -ktlint = "13.1.0" -kover = "0.9.1" -mavenPublish = "0.34.0" -binaryCompatibilityValidatorPlugin = "0.18.1" +atomicfu = "0.33.0" +binaryCompatibilityValidatorPlugin = "0.18.2" +detekt = "2.0.0-alpha.6" +dokka = "2.2.0" +kotlin = "2.4.10" +kover = "0.9.9" +ktlint = "14.2.0" +knit = "0.5.1" +mavenPublish = "0.37.0" +netty = "4.2.18.Final" # libraries version -serialization = "1.9.0" -collections-immutable = "0.4.0" -coroutines = "1.10.2" -kotlinx-io = "0.8.0" -ktor = "3.3.0" -logging = "7.0.13" -slf4j = "2.0.17" -kotest = "6.0.3" awaitility = "4.3.0" - -# Samples -mcp-kotlin = "0.7.2-alpha3" -anthropic = "2.7.0" -shadow = "8.1.1" +collections-immutable = "0.5.2" +coroutines = "1.11.0" +kotest = "6.2.5" +kotlinx-io = "0.9.1" +ktor = "3.5.2" +logging = "8.0.4" +mockk = "1.14.11" +serialization = "1.11.0" +slf4j = "2.0.19" +logback = "1.6.3" +junit = "5.14.4" [libraries] # Plugins +detekt-gradle = { module = "dev.detekt:detekt-gradle-plugin", version.ref = "detekt" } +dokka-gradle = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } kotlin-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" } kotlinx-atomicfu-gradle = { module = "org.jetbrains.kotlinx:atomicfu-gradle-plugin", version.ref = "atomicfu" } -dokka-gradle = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } maven-publish = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "mavenPublish" } # Kotlinx libraries -kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" } +kotlin-logging = { group = "io.github.oshai", name = "kotlin-logging", version.ref = "logging" } +kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "collections-immutable" } kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" } kotlinx-coroutines-core-wasm = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core-wasm-js", version.ref = "coroutines" } kotlinx-io-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-io-core", version.ref = "kotlinx-io" } -kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "collections-immutable" } -kotlin-logging = { group = "io.github.oshai", name = "kotlin-logging", version.ref = "logging" } +kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" } # Ktor +ktor-client-apache5 = { group = "io.ktor", name = "ktor-client-apache5", version.ref = "ktor" } +ktor-client-auth = { group = "io.ktor", name = "ktor-client-auth", version.ref = "ktor" } ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" } +ktor-client-logging = { group = "io.ktor", name = "ktor-client-logging", version.ref = "ktor" } +ktor-server-content-negotiation = { group = "io.ktor", name = "ktor-server-content-negotiation", version.ref = "ktor" } +ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" } +ktor-serialization = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" } +ktor-server-auth = { group = "io.ktor", name = "ktor-server-auth", version.ref = "ktor" } +ktor-server-core = { group = "io.ktor", name = "ktor-server-core", version.ref = "ktor" } ktor-server-sse = { group = "io.ktor", name = "ktor-server-sse", version.ref = "ktor" } ktor-server-websockets = { group = "io.ktor", name = "ktor-server-websockets", version.ref = "ktor" } -ktor-server-core = { group = "io.ktor", name = "ktor-server-core", version.ref = "ktor" } # Testing awaitility = { group = "org.awaitility", name = "awaitility-kotlin", version.ref = "awaitility" } +kotest-assertions-core = { group = "io.kotest", name = "kotest-assertions-core", version.ref = "kotest" } kotest-assertions-json = { group = "io.kotest", name = "kotest-assertions-json", version.ref = "kotest" } +kotest-assertions-ktor = { group = "io.kotest", name = "kotest-assertions-ktor", version.ref = "kotest" } kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" } ktor-client-mock = { group = "io.ktor", name = "ktor-client-mock", version.ref = "ktor" } ktor-server-test-host = { group = "io.ktor", name = "ktor-server-test-host", version.ref = "ktor" } +mockk = { module = "io.mockk:mockk", version.ref = "mockk" } +netty-bom = { group = "io.netty", name = "netty-bom", version.ref = "netty" } slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } +logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" } +junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" } # Samples ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor" } ktor-server-cio = { group = "io.ktor", name = "ktor-server-cio", version.ref = "ktor" } -mcp-kotlin-client = { group = "io.modelcontextprotocol", name = "kotlin-sdk-client", version.ref = "mcp-kotlin" } -mcp-kotlin-server = { group = "io.modelcontextprotocol", name = "kotlin-sdk-server", version.ref = "mcp-kotlin" } -anthropic-java = { group = "com.anthropic", name = "anthropic-java", version.ref = "anthropic" } -ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" } -ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" } [plugins] kotlinx-binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binaryCompatibilityValidatorPlugin" } -ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } - -# Samples -kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } -kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } -kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } -shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" } +ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } +knit = { id = "org.jetbrains.kotlinx.knit", version.ref = "knit" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 1b33c55..eddabd2 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d4081da..ad7845b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,9 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip networkTimeout=10000 +retries=0 +retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 23d15a9..249efbb 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -29,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -114,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -172,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -212,7 +210,6 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" diff --git a/gradlew.bat b/gradlew.bat index 5eed7ee..8508ef6 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -19,12 +19,12 @@ @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -51,7 +51,7 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -65,30 +65,18 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :execute @rem Setup the command line -set CLASSPATH= -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/kotlin-sdk-test/build.gradle.kts b/integration-test/build.gradle.kts similarity index 54% rename from kotlin-sdk-test/build.gradle.kts rename to integration-test/build.gradle.kts index 012619b..7a3a4a3 100644 --- a/kotlin-sdk-test/build.gradle.kts +++ b/integration-test/build.gradle.kts @@ -1,10 +1,11 @@ plugins { id("mcp.multiplatform") + id("mcp.detekt") } kotlin { jvm { - testRuns["test"].executionTask.configure { + tasks.withType { useJUnitPlatform() } } @@ -13,19 +14,25 @@ kotlin { dependencies { implementation(project(":kotlin-sdk")) implementation(kotlin("test")) + implementation(libs.kotest.assertions.core) implementation(libs.kotest.assertions.json) implementation(libs.kotlin.logging) implementation(libs.kotlinx.coroutines.test) implementation(libs.ktor.client.cio) implementation(libs.ktor.server.cio) + implementation(libs.ktor.server.content.negotiation) + implementation(libs.ktor.serialization) + implementation(libs.ktor.server.websockets) + implementation(libs.ktor.server.auth) implementation(libs.ktor.server.test.host) + implementation(libs.ktor.server.content.negotiation) + implementation(libs.ktor.serialization) } } jvmTest { dependencies { - implementation(kotlin("test-junit5")) - implementation(libs.awaitility) - runtimeOnly(libs.slf4j.simple) + implementation(project(":test-utils")) + implementation(project(":kotlin-sdk-testing")) } } } diff --git a/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt new file mode 100644 index 0000000..099329d --- /dev/null +++ b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt @@ -0,0 +1,1577 @@ +package io.modelcontextprotocol.kotlin.sdk.client + +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import io.modelcontextprotocol.kotlin.sdk.server.ServerSession +import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport +import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport +import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions +import io.modelcontextprotocol.kotlin.sdk.types.BooleanSchema +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageResult +import io.modelcontextprotocol.kotlin.sdk.types.DoubleSchema +import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestFormParams +import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestURLParams +import io.modelcontextprotocol.kotlin.sdk.types.ElicitResult +import io.modelcontextprotocol.kotlin.sdk.types.ElicitationCompleteNotification +import io.modelcontextprotocol.kotlin.sdk.types.ElicitationCompleteNotificationParams +import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest +import io.modelcontextprotocol.kotlin.sdk.types.InitializeResult +import io.modelcontextprotocol.kotlin.sdk.types.IntegerSchema +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse +import io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION +import io.modelcontextprotocol.kotlin.sdk.types.ListResourcesRequest +import io.modelcontextprotocol.kotlin.sdk.types.ListResourcesResult +import io.modelcontextprotocol.kotlin.sdk.types.ListRootsRequest +import io.modelcontextprotocol.kotlin.sdk.types.ListToolsRequest +import io.modelcontextprotocol.kotlin.sdk.types.ListToolsResult +import io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel +import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification +import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotificationParams +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.Role +import io.modelcontextprotocol.kotlin.sdk.types.Root +import io.modelcontextprotocol.kotlin.sdk.types.RootsListChangedNotification +import io.modelcontextprotocol.kotlin.sdk.types.SUPPORTED_PROTOCOL_VERSIONS +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.StringSchema +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.TitledMultiSelectEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.Tool +import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema +import io.modelcontextprotocol.kotlin.sdk.types.UntitledMultiSelectEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.UntitledSingleSelectEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.UrlElicitationRequiredException +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withTimeout +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.coroutines.cancellation.CancellationException +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertIs +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlin.test.fail +import kotlin.time.Duration.Companion.seconds + +class ClientTest { + @Test + fun `should initialize with matching protocol version`() = runTest { + var initialised = false + val clientTransport = object : AbstractTransport() { + override suspend fun start() {} + + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { + if (message !is JSONRPCRequest) return + initialised = true + val result = InitializeResult( + protocolVersion = LATEST_PROTOCOL_VERSION, + capabilities = ServerCapabilities(), + serverInfo = Implementation( + name = "test", + version = "1.0", + ), + ) + + val response = JSONRPCResponse( + id = message.id, + result = result, + ) + + _onMessage.invoke(response) + } + + override suspend fun close() { + } + } + + val client = Client( + clientInfo = Implementation( + name = "test client", + version = "1.0", + ), + options = ClientOptions( + capabilities = ClientCapabilities( + sampling = ClientCapabilities.sampling, + ), + ), + ) + + client.connect(clientTransport) + assertTrue(initialised) + } + + @Test + fun `should initialize with supported older protocol version`() = runTest { + val oldVersion = SUPPORTED_PROTOCOL_VERSIONS[1] + val clientTransport = object : AbstractTransport() { + override suspend fun start() {} + + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { + if (message !is JSONRPCRequest) return + check(message.method == Method.Defined.Initialize.value) + + val result = InitializeResult( + protocolVersion = oldVersion, + capabilities = ServerCapabilities(), + serverInfo = Implementation( + name = "test", + version = "1.0", + ), + ) + + val response = JSONRPCResponse( + id = message.id, + result = result, + ) + _onMessage.invoke(response) + } + + override suspend fun close() { + } + } + + val client = Client( + clientInfo = Implementation( + name = "test client", + version = "1.0", + ), + options = ClientOptions( + capabilities = ClientCapabilities( + sampling = ClientCapabilities.sampling, + ), + ), + ) + + client.connect(clientTransport) + assertEquals( + Implementation("test", "1.0"), + client.serverVersion, + ) + } + + @Test + fun `should reject unsupported protocol version`() = runTest { + var closed = false + val clientTransport = object : AbstractTransport() { + override suspend fun start() {} + + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { + if (message !is JSONRPCRequest) return + check(message.method == Method.Defined.Initialize.value) + + val result = InitializeResult( + protocolVersion = "invalid-version", + capabilities = ServerCapabilities(), + serverInfo = Implementation( + name = "test", + version = "1.0", + ), + ) + + val response = JSONRPCResponse( + id = message.id, + result = result, + ) + + _onMessage.invoke(response) + } + + override suspend fun close() { + closed = true + } + } + + val client = Client( + clientInfo = Implementation( + name = "test client", + version = "1.0", + ), + options = ClientOptions(), + ) + + assertFailsWith("Server's protocol version is not supported: invalid-version") { + client.connect(clientTransport) + } + + assertTrue(closed) + } + + @Test + fun `should reject due to non cancellation exception`() = runTest { + var closed = false + val failingTransport = object : AbstractTransport() { + override suspend fun start() {} + + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { + if (message !is JSONRPCRequest) return + check(message.method == Method.Defined.Initialize.value) + throw IllegalStateException("Test error") + } + + override suspend fun close() { + closed = true + } + } + + val client = Client( + clientInfo = Implementation( + name = "test client", + version = "1.0", + ), + options = ClientOptions(), + ) + + val exception = assertFailsWith { + client.connect(failingTransport) + } + + assertEquals("Error connecting to transport: Test error", exception.message) + + assertTrue(closed) + } + + @Test + fun `should rethrow McpException as is`() = runTest { + var closed = false + val failingTransport = object : AbstractTransport() { + override suspend fun start() {} + + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { + if (message !is JSONRPCRequest) return + check(message.method == Method.Defined.Initialize.value) + throw McpException( + code = -32600, + message = "Invalid Request", + ) + } + + override suspend fun close() { + closed = true + } + } + + val client = Client( + clientInfo = Implementation( + name = "test client", + version = "1.0", + ), + options = ClientOptions(), + ) + + val exception = assertFailsWith { + client.connect(failingTransport) + } + + exception.code shouldBe -32600 + exception.message shouldBe "Invalid Request" + + assertTrue(closed) + } + + @Test + fun `should rethrow StreamableHttpError as is`() = runTest { + var closed = false + val failingTransport = object : AbstractTransport() { + override suspend fun start() {} + + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { + if (message !is JSONRPCRequest) return + check(message.method == Method.Defined.Initialize.value) + throw StreamableHttpError( + code = 500, + message = "Internal Server Error", + ) + } + + override suspend fun close() { + closed = true + } + } + + val client = Client( + clientInfo = Implementation( + name = "test client", + version = "1.0", + ), + options = ClientOptions(), + ) + + val exception = assertFailsWith { + client.connect(failingTransport) + } + + assertEquals(500, exception.code) + assertEquals("Streamable HTTP error: Internal Server Error", exception.message) + + assertTrue(closed) + } + + @Test + fun `should respect server capabilities`() = runTest { + val serverOptions = ServerOptions( + capabilities = ServerCapabilities( + resources = ServerCapabilities.Resources(null, null), + tools = ServerCapabilities.Tools(null), + ), + ) + val server = Server( + Implementation(name = "test server", version = "1.0"), + serverOptions, + ) + + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + + val client = Client( + clientInfo = Implementation(name = "test client", version = "1.0"), + options = ClientOptions( + capabilities = ClientCapabilities(sampling = ClientCapabilities.sampling), + ), + ) + + val serverSessionResult = CompletableDeferred() + + listOf( + launch { + client.connect(clientTransport) + }, + launch { + serverSessionResult.complete(server.createSession(serverTransport)) + }, + ).joinAll() + + val serverSession = serverSessionResult.await() + serverSession.setRequestHandler(Method.Defined.Initialize) { _, _ -> + InitializeResult( + protocolVersion = LATEST_PROTOCOL_VERSION, + capabilities = ServerCapabilities( + resources = ServerCapabilities.Resources(null, null), + tools = ServerCapabilities.Tools(null), + ), + serverInfo = Implementation(name = "test", version = "1.0"), + ) + } + + serverSession.setRequestHandler(Method.Defined.ResourcesList) { _, _ -> + ListResourcesResult(resources = emptyList(), nextCursor = null) + } + + serverSession.setRequestHandler(Method.Defined.ToolsList) { _, _ -> + ListToolsResult(tools = emptyList(), nextCursor = null) + } + // Server supports resources and tools, but not prompts + val caps = client.serverCapabilities + assertEquals(ServerCapabilities.Resources(null, null), caps?.resources) + assertEquals(ServerCapabilities.Tools(null), caps?.tools) + assertTrue(caps?.prompts == null) // or check that prompts are absent + + // These should not throw + client.listResources() + client.listTools() + + // This should fail because prompts are not supported + val ex = assertFailsWith { + client.listPrompts() + } + assertTrue(ex.message?.contains("Server does not support prompts") == true) + } + + @Test + fun `should respect client notification capabilities`() = runTest { + val server = Server( + Implementation(name = "test server", version = "1.0"), + ServerOptions(capabilities = ServerCapabilities()), + ) + + val client = Client( + clientInfo = Implementation(name = "test client", version = "1.0"), + options = ClientOptions( + capabilities = ClientCapabilities( + roots = ClientCapabilities.Roots(listChanged = true), + ), + ), + ) + + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + + listOf( + launch { + client.connect(clientTransport) + println("Client connected") + }, + launch { + server.createSession(serverTransport) + println("Server connected") + }, + ).joinAll() + + // This should not throw because the client supports roots.listChanged + client.sendRootsListChanged() + + // Create a new client without the roots.listChanged capability + val clientWithoutCapability = Client( + clientInfo = Implementation(name = "test client without capability", version = "1.0"), + options = ClientOptions( + capabilities = ClientCapabilities(), + ), + ) + + val (clientTransport2, serverTransport2) = InMemoryTransport.createLinkedPair() + listOf( + launch { clientWithoutCapability.connect(clientTransport2) }, + launch { server.createSession(serverTransport2) }, + ).joinAll() + + // This should fail + val ex = assertFailsWith { + clientWithoutCapability.sendRootsListChanged() + } + assertTrue(ex.message?.startsWith("Client does not support") == true) + } + + @Test + fun `should respect server notification capabilities`() = runTest { + val server = Server( + Implementation(name = "test server", version = "1.0"), + ServerOptions( + capabilities = ServerCapabilities( + logging = EmptyJsonObject, + resources = ServerCapabilities.Resources(listChanged = true, subscribe = null), + ), + ), + ) + + val client = Client( + clientInfo = Implementation(name = "test client", version = "1.0"), + options = ClientOptions( + capabilities = ClientCapabilities(), + ), + ) + + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + + val serverSessionResult = CompletableDeferred() + + listOf( + launch { + client.connect(clientTransport) + println("Client connected") + }, + launch { + serverSessionResult.complete(server.createSession(serverTransport)) + println("Server connected") + }, + ).joinAll() + + val serverSession = serverSessionResult.await() + // These should not throw + val jsonObject = buildJsonObject { + put("name", "John") + put("age", 30) + put("isStudent", false) + } + serverSession.sendLoggingMessage( + LoggingMessageNotification( + params = LoggingMessageNotificationParams( + level = LoggingLevel.Info, + data = jsonObject, + ), + ), + ) + serverSession.sendResourceListChanged() + + // This should fail because the server doesn't have the tools capability + val ex = assertFailsWith { + serverSession.sendToolListChanged() + } + assertTrue(ex.message?.contains("Server does not support notifying of tool list changes") == true) + } + + @Test + fun `should handle client cancelling a request`() = runTest { + val server = Server( + Implementation(name = "test server", version = "1.0"), + ServerOptions( + capabilities = ServerCapabilities( + resources = ServerCapabilities.Resources( + listChanged = null, + subscribe = null, + ), + ), + ), + ) + + val def = CompletableDeferred() + val defTimeOut = CompletableDeferred() + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + + val client = Client( + clientInfo = Implementation(name = "test client", version = "1.0"), + options = ClientOptions(capabilities = ClientCapabilities()), + ) + + val serverSessionResult = CompletableDeferred() + + listOf( + launch { + client.connect(clientTransport) + println("Client connected") + }, + launch { + serverSessionResult.complete(server.createSession(serverTransport)) + println("Server connected") + }, + ).joinAll() + + val serverSession = serverSessionResult.await() + + // Register the tap after createSession so it chains onto the session's subscription instead + // of pre-empting the handshake. + val cancelledOnWire = CompletableDeferred() + serverTransport.onMessage { message -> + if (message is JSONRPCNotification && message.method == Method.Defined.NotificationsCancelled.value) { + cancelledOnWire.complete(Unit) + } + } + + serverSession.setRequestHandler(Method.Defined.ResourcesList) { _, _ -> + // Simulate delay + def.complete(Unit) + try { + delay(1000) + } catch (e: CancellationException) { + defTimeOut.complete(Unit) + throw e + } + ListResourcesResult(resources = emptyList()) + fail("Shouldn't have been called") + } + + val defCancel = CompletableDeferred() + val job = launch { + try { + client.listResources() + } catch (e: CancellationException) { + defCancel.complete(Unit) + assertEquals("Cancelled by test", e.message) + } + } + def.await() + runCatching { job.cancel("Cancelled by test") } + defCancel.await() + cancelledOnWire.await() + defTimeOut.await() + } + + @Test + fun `should handle request timeout`() = runTest { + val server = Server( + Implementation(name = "test server", version = "1.0"), + ServerOptions( + capabilities = ServerCapabilities( + resources = ServerCapabilities.Resources( + listChanged = null, + subscribe = null, + ), + ), + ), + ) + + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + val client = Client( + clientInfo = Implementation(name = "test client", version = "1.0"), + options = ClientOptions(capabilities = ClientCapabilities()), + ) + + val serverSessionResult = CompletableDeferred() + + listOf( + launch { + client.connect(clientTransport) + println("Client connected") + }, + launch { + serverSessionResult.complete(server.createSession(serverTransport)) + println("Server connected") + }, + ).joinAll() + + val serverSession = serverSessionResult.await() + serverSession.setRequestHandler(Method.Defined.ResourcesList) { _, _ -> + // Never respond: the handler runs on a real dispatcher while the 1 ms timeout runs on the + // virtual test clock, so a real-time delay could race it. Parking keeps the test deterministic. + awaitCancellation() + } + + // Request with 1 msec timeout should fail immediately + val ex = assertFailsWith { + withTimeout(1) { + client.listResources() + } + } + assertTrue(ex is TimeoutCancellationException) + } + + @Test + fun `should only allow setRequestHandler for declared capabilities`() = runTest { + val client = Client( + clientInfo = Implementation( + name = "test client", + version = "1.0", + ), + options = ClientOptions( + capabilities = ClientCapabilities( + sampling = ClientCapabilities.sampling, + ), + ), + ) + + client.setRequestHandler(Method.Defined.SamplingCreateMessage) { _, _ -> + CreateMessageResult( + role = Role.Assistant, + content = TextContent(text = "Test response"), + model = "test-model", + ) + } + + assertFailsWith("Client does not support roots capability (required for RootsList)") { + client.setRequestHandler(Method.Defined.RootsList) { _, _ -> null } + } + } + + @Test + fun `JSONRPCRequest with ToolsList method and default params returns list of tools`() = runTest { + val serverOptions = ServerOptions( + capabilities = ServerCapabilities( + tools = ServerCapabilities.Tools(null), + ), + ) + val server = Server( + Implementation(name = "test server", version = "1.0"), + serverOptions, + ) + + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + + val client = Client( + clientInfo = Implementation(name = "test client", version = "1.0"), + options = ClientOptions( + capabilities = ClientCapabilities(sampling = ClientCapabilities.sampling), + ), + ) + + var receivedMessage: JSONRPCMessage? = null + clientTransport.onMessage { msg -> + receivedMessage = msg + } + + val serverSessionResult = CompletableDeferred() + + listOf( + launch { + client.connect(clientTransport) + println("Client connected") + }, + launch { + serverSessionResult.complete(server.createSession(serverTransport)) + println("Server connected") + }, + ).joinAll() + + val serverSession = serverSessionResult.await() + + serverSession.setRequestHandler(Method.Defined.Initialize) { _, _ -> + InitializeResult( + protocolVersion = LATEST_PROTOCOL_VERSION, + capabilities = ServerCapabilities( + resources = ServerCapabilities.Resources(null, null), + tools = ServerCapabilities.Tools(null), + ), + serverInfo = Implementation(name = "test", version = "1.0"), + ) + } + + val serverListToolsResult = ListToolsResult( + tools = listOf( + Tool( + name = "testTool", + title = "testTool title", + description = "testTool description", + annotations = null, + inputSchema = ToolSchema(), + outputSchema = null, + ), + ), + nextCursor = null, + ) + + serverSession.setRequestHandler(Method.Defined.ToolsList) { _, _ -> + serverListToolsResult + } + + val serverCapabilities = client.serverCapabilities + assertEquals(ServerCapabilities.Tools(null), serverCapabilities?.tools) + + val request = JSONRPCRequest( + method = Method.Defined.ToolsList.value, + ) + clientTransport.send(request) + + assertIs(receivedMessage) + val receivedAsResponse = receivedMessage as JSONRPCResponse + assertEquals(request.id, receivedAsResponse.id) + assertEquals(request.jsonrpc, receivedAsResponse.jsonrpc) + assertEquals(serverListToolsResult, receivedAsResponse.result) + } + + @Test + fun `listRoots returns list of roots`() = runTest { + val client = Client( + Implementation(name = "test client", version = "1.0"), + ClientOptions( + capabilities = ClientCapabilities( + roots = ClientCapabilities.Roots(null), + ), + ), + ) + + val clientRoots = listOf( + Root(uri = "file:///test-root", name = "testRoot"), + ) + + client.addRoots(clientRoots) + + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + + val server = Server( + serverInfo = Implementation(name = "test server", version = "1.0"), + options = ServerOptions( + capabilities = ServerCapabilities(), + ), + ) + + val serverSessionResult = CompletableDeferred() + + listOf( + launch { + client.connect(clientTransport) + println("Client connected") + }, + launch { + serverSessionResult.complete(server.createSession(serverTransport)) + println("Server connected") + }, + ).joinAll() + + val serverSession = serverSessionResult.await() + + val clientCapabilities = serverSession.clientCapabilities + assertEquals(ClientCapabilities.Roots(null), clientCapabilities?.roots) + + val listRootsResult = serverSession.listRoots() + + assertEquals(listRootsResult.roots, clientRoots) + } + + @Test + fun `addRoot should throw when roots capability is not supported`() = runTest { + val client = Client( + Implementation(name = "test client", version = "1.0"), + ClientOptions( + capabilities = ClientCapabilities(), + ), + ) + + // Verify that adding a root throws an exception + val exception = assertFailsWith { + client.addRoot(uri = "file:///test-root1", name = "testRoot1") + } + assertEquals("Client does not support roots capability.", exception.message) + } + + @Test + fun `removeRoot should throw when roots capability is not supported`() = runTest { + val client = Client( + Implementation(name = "test client", version = "1.0"), + ClientOptions( + capabilities = ClientCapabilities(), + ), + ) + + // Verify that removing a root throws an exception + val exception = assertFailsWith { + client.removeRoot(uri = "file:///test-root1") + } + assertEquals("Client does not support roots capability.", exception.message) + } + + @Test + fun `removeRoot should remove a root`() = runTest { + val client = Client( + Implementation(name = "test client", version = "1.0"), + ClientOptions( + capabilities = ClientCapabilities( + roots = ClientCapabilities.Roots(null), + ), + ), + ) + + // Add some roots + client.addRoots( + listOf( + Root(uri = "file:///test-root1", name = "testRoot1"), + Root(uri = "file:///test-root2", name = "testRoot2"), + ), + ) + + // Remove a root + val result = client.removeRoot("file:///test-root1") + + // Verify the root was removed + assertTrue(result, "Root should be removed successfully") + } + + @Test + fun `removeRoots should remove multiple roots`() = runTest { + val client = Client( + Implementation(name = "test client", version = "1.0"), + ClientOptions( + capabilities = ClientCapabilities( + roots = ClientCapabilities.Roots(null), + ), + ), + ) + + // Add some roots + client.addRoots( + listOf( + Root(uri = "file:///test-root1", name = "testRoot1"), + Root(uri = "file:///test-root2", name = "testRoot2"), + ), + ) + + // Remove multiple roots + val result = client.removeRoots( + listOf("file:///test-root1", "file:///test-root2"), + ) + + // Verify the root was removed + assertEquals(2, result, "Both roots should be removed") + } + + @Test + fun `sendRootsListChanged should notify server`() = runTest { + val client = Client( + Implementation(name = "test client", version = "1.0"), + ClientOptions( + capabilities = ClientCapabilities( + roots = ClientCapabilities.Roots(listChanged = true), + ), + ), + ) + + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + + val server = Server( + serverInfo = Implementation(name = "test server", version = "1.0"), + options = ServerOptions( + capabilities = ServerCapabilities(), + ), + ) + + // The handler runs on Dispatchers.Default after concurrent dispatch is enabled; await the deferred. + val rootListChangedReceived = CompletableDeferred() + + val serverSessionResult = CompletableDeferred() + + listOf( + launch { + client.connect(clientTransport) + println("Client connected") + }, + launch { + serverSessionResult.complete(server.createSession(serverTransport)) + println("Server connected") + }, + ).joinAll() + + val serverSession = serverSessionResult.await() + serverSession.setNotificationHandler( + Method.Defined.NotificationsRootsListChanged, + ) { + rootListChangedReceived.complete(Unit) + CompletableDeferred(Unit) + } + + client.sendRootsListChanged() + + withTimeout(5.seconds) { rootListChangedReceived.await() } + } + + @Test + fun `should reject server elicitation when elicitation capability is not supported`() = runTest { + val client = Client( + Implementation(name = "test client", version = "1.0"), + ClientOptions( + capabilities = ClientCapabilities(), + ), + ) + + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + + val server = Server( + serverInfo = Implementation(name = "test server", version = "1.0"), + options = ServerOptions( + capabilities = ServerCapabilities(), + ), + ) + + val serverSessionResult = CompletableDeferred() + + listOf( + launch { + client.connect(clientTransport) + println("Client connected") + }, + launch { + serverSessionResult.complete(server.createSession(serverTransport)) + println("Server connected") + }, + ).joinAll() + + val serverSession = serverSessionResult.await() + + // Verify that creating an elicitation throws an exception + val exception = assertFailsWith { + serverSession.createElicitation( + message = "Please provide your GitHub username", + requestedSchema = ElicitRequestParams.RequestedSchema( + properties = mapOf("name" to StringSchema()), + required = listOf("name"), + ), + ) + } + assertEquals( + "Client does not support elicitation (required for elicitation/create)", + exception.message, + ) + } + + @Test + fun `should handle logging setLevel request`() = runTest { + val server = Server( + Implementation(name = "test server", version = "1.0"), + ServerOptions( + capabilities = ServerCapabilities( + logging = EmptyJsonObject, + ), + ), + ) + + val client = Client( + clientInfo = Implementation(name = "test client", version = "1.0"), + options = ClientOptions( + capabilities = ClientCapabilities(), + ), + ) + + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + + val receivedMessages = mutableListOf() + client.setNotificationHandler(Method.Defined.NotificationsMessage) { notification -> + receivedMessages.add(notification) + CompletableDeferred(Unit) + } + + val serverSessionResult = CompletableDeferred() + + listOf( + launch { + client.connect(clientTransport) + println("Client connected") + }, + launch { + serverSessionResult.complete(server.createSession(serverTransport)) + println("Server connected") + }, + ).joinAll() + + val serverSession = serverSessionResult.await() + + // Set logging level to warning + val minLevel = LoggingLevel.Warning + val result = client.setLoggingLevel(minLevel) + assertNull(result.meta) + + // Send messages of different levels + val testMessages = listOf( + LoggingLevel.Debug to "Debug - should be filtered", + LoggingLevel.Info to "Info - should be filtered", + LoggingLevel.Warning to "Warning - should pass", + LoggingLevel.Error to "Error - should pass", + ) + + testMessages.forEach { (level, message) -> + serverSession.sendLoggingMessage( + LoggingMessageNotification( + params = LoggingMessageNotificationParams( + level = level, + data = buildJsonObject { put("message", message) }, + ), + ), + ) + } + + delay(100) + + // Only warning and error should be received + assertEquals(2, receivedMessages.size, "Should receive only 2 messages (warning and error)") + + // Verify all received messages have severity >= minLevel + receivedMessages.forEach { message -> + val messageSeverity = message.params.level.ordinal + assertTrue( + messageSeverity >= minLevel.ordinal, + "Received message with level ${message.params.level} should have severity >= $minLevel", + ) + } + } + + @Test + fun `should handle server elicitation`() = runTest { + val client = Client( + Implementation(name = "test client", version = "1.0"), + ClientOptions( + capabilities = ClientCapabilities( + elicitation = ClientCapabilities.Elicitation(), + ), + ), + ) + + val elicitationMessage = "Please provide your GitHub username" + val requestedSchema = ElicitRequestParams.RequestedSchema( + properties = mapOf("name" to StringSchema()), + required = listOf("name"), + ) + + val elicitationResultAction = ElicitResult.Action.Accept + val elicitationResultContent = buildJsonObject { + put("name", "octocat") + } + + client.setElicitationHandler { request -> + assertEquals(elicitationMessage, request.params.message) + val formParams = request.params as ElicitRequestFormParams + assertEquals(requestedSchema, formParams.requestedSchema) + + ElicitResult( + action = elicitationResultAction, + content = elicitationResultContent, + ) + } + + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + + val server = Server( + serverInfo = Implementation(name = "test server", version = "1.0"), + options = ServerOptions( + capabilities = ServerCapabilities(), + ), + ) + + val serverSessionResult = CompletableDeferred() + + listOf( + launch { + client.connect(clientTransport) + println("Client connected") + }, + launch { + serverSessionResult.complete(server.createSession(serverTransport)) + println("Server connected") + }, + ).joinAll() + + val serverSession = serverSessionResult.await() + + val result = serverSession.createElicitation( + message = elicitationMessage, + requestedSchema = requestedSchema, + ) + + assertEquals(elicitationResultAction, result.action) + assertEquals(elicitationResultContent, result.content) + } + + @Test + fun `should reject accepted elicitation content that does not match requested schema`() = runTest { + val requestedSchema = ElicitRequestParams.RequestedSchema( + properties = mapOf("name" to StringSchema()), + required = listOf("name"), + ) + val (client, serverSession) = setupElicitationPair { + ElicitResult(action = ElicitResult.Action.Accept, content = buildJsonObject { put("name", 42) }) + } + + val exception = assertFailsWith { + serverSession.createElicitation(message = "mismatch", requestedSchema = requestedSchema) + } + assertTrue( + exception.message.orEmpty() + .startsWith("Elicitation response content does not match requested schema: "), + "Unexpected message: ${exception.message}", + ) + assertTrue("'name' must be string" in exception.message.orEmpty(), "Unexpected message: ${exception.message}") + + client.close() + } + + @Test + fun `should reject accepted elicitation without content when schema has required properties`() = runTest { + val requestedSchema = ElicitRequestParams.RequestedSchema( + properties = mapOf("name" to StringSchema()), + required = listOf("name"), + ) + val (client, serverSession) = setupElicitationPair { + ElicitResult(action = ElicitResult.Action.Accept, content = null) + } + + val exception = assertFailsWith { + serverSession.createElicitation(message = "no content", requestedSchema = requestedSchema) + } + assertTrue( + "must have required property 'name'" in exception.message.orEmpty(), + "Unexpected message: ${exception.message}", + ) + + client.close() + } + + @Test + fun `should accept elicitation without content when schema has no required properties`() = runTest { + val requestedSchema = ElicitRequestParams.RequestedSchema( + properties = mapOf("name" to StringSchema()), + ) + val (client, serverSession) = setupElicitationPair { + ElicitResult(action = ElicitResult.Action.Accept, content = null) + } + + val result = serverSession.createElicitation(message = "no required", requestedSchema = requestedSchema) + + assertEquals(ElicitResult.Action.Accept, result.action) + assertNull(result.content) + + client.close() + } + + @Test + fun `should pass through declined elicitation without validation`() = runTest { + val requestedSchema = ElicitRequestParams.RequestedSchema( + properties = mapOf("name" to StringSchema()), + required = listOf("name"), + ) + val (client, serverSession) = setupElicitationPair { + ElicitResult(action = ElicitResult.Action.Decline) + } + + val result = serverSession.createElicitation(message = "decline", requestedSchema = requestedSchema) + + assertEquals(ElicitResult.Action.Decline, result.action) + assertNull(result.content) + + client.close() + } + + @Test + fun `should pass through cancelled elicitation without validation`() = runTest { + val requestedSchema = ElicitRequestParams.RequestedSchema( + properties = mapOf("name" to StringSchema()), + required = listOf("name"), + ) + val (client, serverSession) = setupElicitationPair { + ElicitResult(action = ElicitResult.Action.Cancel) + } + + val result = serverSession.createElicitation(message = "cancel", requestedSchema = requestedSchema) + + assertEquals(ElicitResult.Action.Cancel, result.action) + assertNull(result.content) + + client.close() + } + + @Test + fun `should apply elicitation defaults for missing fields in empty content`() = runTest { + val schema = defaultsTestSchema() + val (client, serverSession) = setupElicitationPair { + ElicitResult(action = ElicitResult.Action.Accept, content = JsonObject(emptyMap())) + } + + val result = serverSession.createElicitation(message = "fill defaults", requestedSchema = schema) + + assertEquals(ElicitResult.Action.Accept, result.action) + val content = result.content!! + assertEquals(JsonPrimitive("John Doe"), content["name"]) + assertEquals(JsonPrimitive(30), content["age"]) + assertEquals(JsonPrimitive(95.5), content["score"]) + assertEquals(JsonPrimitive("active"), content["status"]) + assertEquals(JsonPrimitive(true), content["verified"]) + + client.close() + } + + @Test + fun `should apply elicitation defaults only for missing fields preserving user values`() = runTest { + val schema = defaultsTestSchema() + val userContent = buildJsonObject { put("name", "Custom Name") } + val (client, serverSession) = setupElicitationPair { + ElicitResult(action = ElicitResult.Action.Accept, content = userContent) + } + + val result = serverSession.createElicitation(message = "partial", requestedSchema = schema) + + val content = result.content!! + assertEquals(JsonPrimitive("Custom Name"), content["name"]) + assertEquals(JsonPrimitive(30), content["age"]) + assertEquals(JsonPrimitive(95.5), content["score"]) + assertEquals(JsonPrimitive("active"), content["status"]) + assertEquals(JsonPrimitive(true), content["verified"]) + + client.close() + } + + @Test + fun `should not apply elicitation defaults when action is decline`() = runTest { + val schema = defaultsTestSchema() + val (client, serverSession) = setupElicitationPair { + ElicitResult(action = ElicitResult.Action.Decline) + } + + val result = serverSession.createElicitation(message = "decline", requestedSchema = schema) + + assertEquals(ElicitResult.Action.Decline, result.action) + assertNull(result.content) + + client.close() + } + + @Test + fun `should not apply elicitation defaults when action is cancel`() = runTest { + val schema = defaultsTestSchema() + val (client, serverSession) = setupElicitationPair { + ElicitResult(action = ElicitResult.Action.Cancel) + } + + val result = serverSession.createElicitation(message = "cancel", requestedSchema = schema) + + assertEquals(ElicitResult.Action.Cancel, result.action) + assertNull(result.content) + + client.close() + } + + @Test + fun `should not modify content when schema has no defaults`() = runTest { + val schema = ElicitRequestParams.RequestedSchema( + properties = mapOf( + "name" to StringSchema(description = "name"), + "age" to IntegerSchema(description = "age"), + ), + ) + val emptyContent = JsonObject(emptyMap()) + val (client, serverSession) = setupElicitationPair { + ElicitResult(action = ElicitResult.Action.Accept, content = emptyContent) + } + + val result = serverSession.createElicitation(message = "no defaults", requestedSchema = schema) + + assertEquals(ElicitResult.Action.Accept, result.action) + assertTrue(result.content!!.isEmpty()) + + client.close() + } + + @Test + fun `should apply elicitation defaults for multi-select enum schemas`() = runTest { + val schema = ElicitRequestParams.RequestedSchema( + properties = mapOf( + "tags" to UntitledMultiSelectEnumSchema( + description = "tags", + items = UntitledMultiSelectEnumSchema.Items(enumValues = listOf("a", "b", "c")), + default = listOf("a", "b"), + ), + "options" to TitledMultiSelectEnumSchema( + description = "options", + items = TitledMultiSelectEnumSchema.Items( + anyOf = listOf( + io.modelcontextprotocol.kotlin.sdk.types.EnumOption("x", "X"), + io.modelcontextprotocol.kotlin.sdk.types.EnumOption("y", "Y"), + ), + ), + default = listOf("x"), + ), + ), + ) + val (client, serverSession) = setupElicitationPair { + ElicitResult(action = ElicitResult.Action.Accept, content = JsonObject(emptyMap())) + } + + val result = serverSession.createElicitation(message = "multi-select", requestedSchema = schema) + + val content = result.content!! + val tags = content["tags"]!! + assertIs(tags) + assertEquals(2, tags.size) + assertEquals("a", tags[0].jsonPrimitive.content) + assertEquals("b", tags[1].jsonPrimitive.content) + + val options = content["options"]!! + assertIs(options) + assertEquals(1, options.size) + assertEquals("x", options[0].jsonPrimitive.content) + + client.close() + } + + // ── URL-mode elicitation (SEP-1036) ───────────────────────────────── + + @Test + fun `should handle URL mode elicitation end-to-end`() = runTest { + val client = Client( + Implementation(name = "test client", version = "1.0"), + ClientOptions( + capabilities = ClientCapabilities( + elicitation = ClientCapabilities.Elicitation(url = EmptyJsonObject), + ), + ), + ) + + val elicitationId = "550e8400-e29b-41d4-a716-446655440000" + val url = "https://oauth.example.com/authorize" + + client.setElicitationHandler { request -> + val params = assertIs(request.params) + assertEquals(elicitationId, params.elicitationId) + assertEquals(url, params.url) + ElicitResult(action = ElicitResult.Action.Accept) + } + + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + val server = Server( + serverInfo = Implementation(name = "test server", version = "1.0"), + options = ServerOptions(capabilities = ServerCapabilities()), + ) + + val serverSessionResult = CompletableDeferred() + listOf( + launch { client.connect(clientTransport) }, + launch { serverSessionResult.complete(server.createSession(serverTransport)) }, + ).joinAll() + val serverSession = serverSessionResult.await() + + val result = serverSession.createElicitation( + message = "Authorize access to continue", + elicitationId = elicitationId, + url = url, + ) + + assertEquals(ElicitResult.Action.Accept, result.action) + assertNull(result.content) + + client.close() + } + + @Test + fun `should reject URL mode elicitation when client supports only form mode`() = runTest { + val (client, serverSession) = setupElicitationPair { + ElicitResult(action = ElicitResult.Action.Accept) + } + + val exception = assertFailsWith { + serverSession.createElicitation( + message = "Authorize", + elicitationId = "id-1", + url = "https://example.com/auth", + ) + } + assertTrue(exception.message!!.contains("elicitation.url")) + + client.close() + } + + @Test + fun `should deliver elicitation complete notification to client`() = runTest { + val received = CompletableDeferred() + val client = Client( + Implementation(name = "test client", version = "1.0"), + ClientOptions( + capabilities = ClientCapabilities( + elicitation = ClientCapabilities.Elicitation(url = EmptyJsonObject), + ), + ), + ) + client.setElicitationCompleteHandler { received.complete(it) } + + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + val server = Server( + serverInfo = Implementation(name = "test server", version = "1.0"), + options = ServerOptions(capabilities = ServerCapabilities()), + ) + + val serverSessionResult = CompletableDeferred() + listOf( + launch { client.connect(clientTransport) }, + launch { serverSessionResult.complete(server.createSession(serverTransport)) }, + ).joinAll() + val serverSession = serverSessionResult.await() + + val elicitationId = "complete-id-1" + serverSession.sendElicitationComplete( + ElicitationCompleteNotification(ElicitationCompleteNotificationParams(elicitationId = elicitationId)), + ) + + val notification = received.await() + assertEquals(elicitationId, notification.params.elicitationId) + + client.close() + } + + @Test + fun `should reject elicitation complete when client supports only form mode`() = runTest { + val (client, serverSession) = setupElicitationPair { + ElicitResult(action = ElicitResult.Action.Accept) + } + + val exception = assertFailsWith { + serverSession.sendElicitationComplete( + ElicitationCompleteNotification(ElicitationCompleteNotificationParams(elicitationId = "id-1")), + ) + } + assertTrue(exception.message!!.contains("elicitation.url")) + + client.close() + } + + @Test + fun `setElicitationCompleteHandler should require url capability`() = runTest { + val client = Client( + Implementation(name = "test client", version = "1.0"), + ClientOptions( + capabilities = ClientCapabilities( + elicitation = ClientCapabilities.Elicitation(), + ), + ), + ) + + assertFailsWith { + client.setElicitationCompleteHandler { } + } + } + + @Test + fun `should surface URL elicitation required error to client as typed exception`() = runTest { + val client = Client( + Implementation(name = "test client", version = "1.0"), + ClientOptions( + capabilities = ClientCapabilities( + elicitation = ClientCapabilities.Elicitation(url = EmptyJsonObject), + ), + ), + ) + + val elicitationId = "auth-required-1" + val url = "https://oauth.example.com/authorize" + + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + val server = Server( + serverInfo = Implementation(name = "test server", version = "1.0"), + options = ServerOptions(capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(true))), + ) + server.addTool("needs-auth", "Requires URL elicitation") { + throw UrlElicitationRequiredException( + listOf( + ElicitRequestURLParams( + message = "Authorize to continue", + elicitationId = elicitationId, + url = url, + ), + ), + ) + } + + val serverSessionResult = CompletableDeferred() + listOf( + launch { client.connect(clientTransport) }, + launch { serverSessionResult.complete(server.createSession(serverTransport)) }, + ).joinAll() + serverSessionResult.await() + + val exception = assertFailsWith { + client.callTool(name = "needs-auth", arguments = emptyMap()) + } + val elicitation = exception.elicitations.single() + assertEquals(elicitationId, elicitation.elicitationId) + assertEquals(url, elicitation.url) + + client.close() + } + + private fun defaultsTestSchema(): ElicitRequestParams.RequestedSchema = ElicitRequestParams.RequestedSchema( + properties = mapOf( + "name" to StringSchema(description = "User name", default = "John Doe"), + "age" to IntegerSchema(description = "User age", default = 30), + "score" to DoubleSchema(description = "User score", default = 95.5), + "status" to UntitledSingleSelectEnumSchema( + description = "User status", + enumValues = listOf("active", "inactive", "pending"), + default = "active", + ), + "verified" to BooleanSchema(description = "Verification status", default = true), + ), + ) + + private suspend fun setupElicitationPair( + handler: (io.modelcontextprotocol.kotlin.sdk.types.ElicitRequest) -> ElicitResult, + ): Pair = kotlinx.coroutines.coroutineScope { + val client = Client( + Implementation(name = "test client", version = "1.0"), + ClientOptions(capabilities = ClientCapabilities(elicitation = ClientCapabilities.Elicitation())), + ) + client.setElicitationHandler(handler) + + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + val server = Server( + serverInfo = Implementation(name = "test server", version = "1.0"), + options = ServerOptions(capabilities = ServerCapabilities()), + ) + + val serverSessionResult = CompletableDeferred() + listOf( + launch { client.connect(clientTransport) }, + launch { serverSessionResult.complete(server.createSession(serverTransport)) }, + ).joinAll() + + client to serverSessionResult.await() + } +} diff --git a/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt similarity index 70% rename from kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt rename to integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt index 4b49e1e..8abf75d 100644 --- a/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt +++ b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt @@ -6,15 +6,16 @@ import io.ktor.server.cio.CIO import io.ktor.server.engine.EmbeddedServer import io.ktor.server.engine.embeddedServer import io.ktor.server.routing.routing -import io.modelcontextprotocol.kotlin.sdk.Implementation -import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions import io.modelcontextprotocol.kotlin.sdk.server.mcp import io.modelcontextprotocol.kotlin.sdk.shared.BaseTransportTest +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext import kotlin.test.BeforeTest -import kotlin.test.Ignore import kotlin.test.Test import io.ktor.client.plugins.sse.SSE as ClientSSE import io.ktor.server.sse.SSE as ServerSSE @@ -47,7 +48,7 @@ class SseTransportTest : BaseTransportTest() { val actualPort = server.actualPort() - val client = HttpClient { + val transport = HttpClient { install(ClientSSE) }.mcpSseTransport { url { @@ -57,38 +58,24 @@ class SseTransportTest : BaseTransportTest() { } try { - testClientOpenClose(client) + testTransportOpenClose(transport) } finally { server.stopSuspend() } } - @Ignore @Test fun `should read messages`() = runTest { val server = embeddedServer(CIO, port = 0) { install(ServerSSE) routing { mcp { mcpServer } -// sse { -// mcpSseTransport("", transports).apply { -// onMessage { -// send(it) -// } -// -// start() -// } -// } -// -// post { -// mcpPostEndpoint(transports) -// } } }.startSuspend(wait = false) val actualPort = server.actualPort() - val client = HttpClient { + val transport = HttpClient { install(ClientSSE) }.mcpSseTransport { url { @@ -97,41 +84,34 @@ class SseTransportTest : BaseTransportTest() { } } + val client = Client( + clientInfo = Implementation(name = "test-client", version = "1.0"), + options = ClientOptions(), + ) + try { - testClientRead(client) + withContext(Dispatchers.Default) { + client.connect(transport) + client.ping() + } } finally { + client.close() server.stopSuspend() } } - @Ignore @Test fun `test sse path not root path`() = runTest { val server = embeddedServer(CIO, port = 0) { install(ServerSSE) routing { mcp("/sse") { mcpServer } -// route("/sse") { -// sse { -// mcpSseTransport("", transports).apply { -// onMessage { -// send(it) -// } -// -// start() -// } -// } -// -// post { -// mcpPostEndpoint(transports) -// } -// } } }.startSuspend(wait = false) val actualPort = server.actualPort() - val client = HttpClient { + val transport = HttpClient { install(ClientSSE) }.mcpSseTransport { url { @@ -141,9 +121,18 @@ class SseTransportTest : BaseTransportTest() { } } + val client = Client( + clientInfo = Implementation(name = "test-client", version = "1.0"), + options = ClientOptions(), + ) + try { - testClientRead(client) + withContext(Dispatchers.Default) { + client.connect(transport) + client.ping() + } } finally { + client.close() server.stopSuspend() } } diff --git a/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketTransportTest.kt b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketTransportTest.kt similarity index 92% rename from kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketTransportTest.kt rename to integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketTransportTest.kt index a4bf7a3..9733071 100644 --- a/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketTransportTest.kt +++ b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketTransportTest.kt @@ -22,7 +22,7 @@ class WebSocketTransportTest : BaseTransportTest() { install(io.ktor.client.plugins.websocket.WebSockets) }.mcpWebSocketTransport() - testClientOpenClose(client) + testTransportOpenClose(client) } @Test @@ -41,11 +41,11 @@ class WebSocketTransportTest : BaseTransportTest() { } } - val client = createClient { + val transport = createClient { install(io.ktor.client.plugins.websocket.WebSockets) }.mcpWebSocketTransport() - testClientRead(client) + testTransportRead(transport) clientFinished.complete(Unit) } diff --git a/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/InMemoryTransportTest.kt b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/InMemoryTransportTest.kt new file mode 100644 index 0000000..83e5618 --- /dev/null +++ b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/InMemoryTransportTest.kt @@ -0,0 +1,206 @@ +package io.modelcontextprotocol.kotlin.sdk.integration + +import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport +import io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams +import io.modelcontextprotocol.kotlin.sdk.types.InitializedNotification +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.PromptListChangedNotification +import io.modelcontextprotocol.kotlin.sdk.types.ResourceListChangedNotification +import io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotification +import io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotificationParams +import io.modelcontextprotocol.kotlin.sdk.types.ToolListChangedNotification +import io.modelcontextprotocol.kotlin.sdk.types.toJSON +import kotlinx.coroutines.test.runTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class InMemoryTransportTest { + private lateinit var clientTransport: InMemoryTransport + private lateinit var serverTransport: InMemoryTransport + + @BeforeTest + fun setUp() { + val (client, server) = InMemoryTransport.createLinkedPair() + clientTransport = client + serverTransport = server + } + + @Test + fun `should create linked pair`() { + assertNotNull(clientTransport) + assertNotNull(serverTransport) + } + + @Test + fun `should start without error`() = runTest { + clientTransport.start() + serverTransport.start() + // If no exception is thrown, the test passes + } + + @Test + fun `should send message from client to server`() = runTest { + val message = InitializedNotification() + + var receivedMessage: JSONRPCMessage? = null + serverTransport.onMessage { msg -> + receivedMessage = msg + } + + val rpcNotification = message.toJSON() + clientTransport.send(rpcNotification) + assertEquals(rpcNotification, receivedMessage) + } + + @Test + fun `should send message from server to client`() = runTest { + val message = InitializedNotification() + .toJSON() + + var receivedMessage: JSONRPCMessage? = null + clientTransport.onMessage { msg -> + receivedMessage = msg + } + + serverTransport.send(message) + assertEquals(message, receivedMessage) + } + + @Test + fun `should handle close`() = runTest { + var clientClosed = false + var serverClosed = false + + clientTransport.onClose { + clientClosed = true + } + + serverTransport.onClose { + serverClosed = true + } + + clientTransport.close() + assertTrue(clientClosed) + assertTrue(serverClosed) + } + + @Test + fun `should throw error when sending after close`() = runTest { + clientTransport.close() + + assertFailsWith { + clientTransport.send( + InitializedNotification().toJSON(), + ) + } + } + + @Test + fun `should queue messages sent before start`() = runTest { + val message = InitializedNotification() + .toJSON() + + var receivedMessage: JSONRPCMessage? = null + serverTransport.onMessage { msg -> + receivedMessage = msg + } + + clientTransport.send(message) + serverTransport.start() + assertEquals(message, receivedMessage) + } + + @Test + fun `should send ToolListChangedNotification from server to client`() = runTest { + val notification = ToolListChangedNotification( + BaseNotificationParams(), + ) + + var receivedMessage: JSONRPCMessage? = null + clientTransport.onMessage { msg -> + receivedMessage = msg + } + + val rpcMessage = notification.toJSON() + serverTransport.send(rpcMessage) + assertEquals(rpcMessage, receivedMessage) + } + + @Test + fun `should send PromptListChangedNotification from server to client`() = runTest { + val notification = PromptListChangedNotification( + BaseNotificationParams(), + ) + + var receivedMessage: JSONRPCMessage? = null + clientTransport.onMessage { msg -> + receivedMessage = msg + } + + val rpcMessage = notification.toJSON() + serverTransport.send(rpcMessage) + assertEquals(rpcMessage, receivedMessage) + } + + @Test + fun `should send ResourceListChangedNotification from server to client`() = runTest { + val notification = ResourceListChangedNotification( + BaseNotificationParams(), + ) + + var receivedMessage: JSONRPCMessage? = null + clientTransport.onMessage { msg -> + receivedMessage = msg + } + + val rpcMessage = notification.toJSON() + serverTransport.send(rpcMessage) + assertEquals(rpcMessage, receivedMessage) + } + + @Test + fun `should send ResourceUpdatedNotification from server to client`() = runTest { + val notification = ResourceUpdatedNotification( + ResourceUpdatedNotificationParams( + uri = "file:///workspace/data.json", + ), + ) + + var receivedMessage: JSONRPCMessage? = null + clientTransport.onMessage { msg -> + receivedMessage = msg + } + + val rpcMessage = notification.toJSON() + serverTransport.send(rpcMessage) + assertEquals(rpcMessage, receivedMessage) + } + + @Test + fun `should handle multiple notifications in sequence`() = runTest { + val notifications = listOf( + ToolListChangedNotification(), + PromptListChangedNotification(), + ResourceListChangedNotification(), + ResourceUpdatedNotification(ResourceUpdatedNotificationParams(uri = "file:///workspace/data.json")), + ) + + val receivedMessages = mutableListOf() + clientTransport.onMessage { msg -> + receivedMessages.add(msg) + } + + notifications.forEach { notification -> + serverTransport.send(notification.toJSON()) + } + + assertEquals(notifications.size, receivedMessages.size) + notifications.forEachIndexed { index, notification -> + assertEquals(notification.toJSON(), receivedMessages[index]) + } + } +} diff --git a/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/BaseTransportTest.kt b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/BaseTransportTest.kt new file mode 100644 index 0000000..a6c6e3d --- /dev/null +++ b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/BaseTransportTest.kt @@ -0,0 +1,71 @@ +package io.modelcontextprotocol.kotlin.sdk.shared + +import io.kotest.assertions.nondeterministic.eventually +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.types.InitializedNotification +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.PingRequest +import io.modelcontextprotocol.kotlin.sdk.types.toJSON +import kotlinx.coroutines.CompletableDeferred +import kotlin.test.fail +import kotlin.time.Duration.Companion.seconds + +abstract class BaseTransportTest { + + protected suspend fun testTransportOpenClose(transport: Transport) { + transport.onError { error -> + fail("Unexpected error: $error") + } + + var didClose = false + transport.onClose { didClose = true } + + transport.start() + + // Verify transport stays open after start (use eventually for cross-platform reliability) + eventually(2.seconds) { + didClose shouldBe false + } + + transport.close() + + // Verify transport is closed after close() call + eventually(2.seconds) { + didClose shouldBe true + } + } + + protected suspend fun testTransportRead(transport: Transport) { + transport.onError { error -> + error.printStackTrace() + fail("Unexpected error: $error") + } + + val messages = listOf( + PingRequest().toJSON(), + InitializedNotification().toJSON(), + ) + + val readMessages = mutableListOf() + val finished = CompletableDeferred() + + transport.onMessage { message -> + readMessages.add(message) + if (message == messages.last()) { + finished.complete(Unit) + } + } + + transport.start() + + for (message in messages) { + transport.send(message) + } + + finished.await() + + messages shouldBe readMessages + + transport.close() + } +} diff --git a/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/InMemoryTransport.kt b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/InMemoryTransport.kt similarity index 84% rename from kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/InMemoryTransport.kt rename to integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/InMemoryTransport.kt index fd567c7..2c15635 100644 --- a/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/InMemoryTransport.kt +++ b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/InMemoryTransport.kt @@ -1,6 +1,6 @@ package io.modelcontextprotocol.kotlin.sdk.shared -import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage /** * In-memory transport for creating clients and servers that talk to each other within the same process. @@ -36,11 +36,11 @@ class InMemoryTransport : AbstractTransport() { val other = otherTransport otherTransport = null other?.close() - _onClose.invoke() + invokeOnCloseCallback() } - override suspend fun send(message: JSONRPCMessage) { - val other = otherTransport ?: throw IllegalStateException("Not connected") + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { + val other = checkNotNull(otherTransport) { "Not connected" } other._onMessage.invoke(message) } diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt new file mode 100644 index 0000000..fc3c1d3 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt @@ -0,0 +1,211 @@ +package io.modelcontextprotocol.kotlin.sdk.client + +import io.kotest.assertions.nondeterministic.eventually +import io.modelcontextprotocol.kotlin.sdk.shared.BaseTransportTest +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.test.utils.createSleepyProcessBuilder +import io.modelcontextprotocol.kotlin.test.utils.createTeeProcessBuilder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.withTimeoutOrNull +import kotlinx.io.Buffer +import kotlinx.io.RawSource +import kotlinx.io.asSink +import kotlinx.io.asSource +import kotlinx.io.buffered +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Timeout +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.condition.DisabledOnOs +import org.junit.jupiter.api.condition.OS +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import kotlin.concurrent.atomics.AtomicBoolean +import kotlin.concurrent.atomics.ExperimentalAtomicApi +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import kotlin.test.fail +import kotlin.time.Duration.Companion.seconds + +@Timeout(30, unit = TimeUnit.SECONDS) +@DisabledOnOs(OS.WINDOWS) // TODO: fix running on windows +class StdioClientTransportTest : BaseTransportTest() { + + @Test + fun `handle stdio error`(): Unit = runBlocking(Dispatchers.IO) { + val processBuilder = createSleepyProcessBuilder() + + val process = processBuilder.start() + + val stdin = process.inputStream.asSource().buffered() + val stdout = process.outputStream.asSink().buffered() + val stderr = process.errorStream.asSource().buffered() + + val transport = StdioClientTransport( + input = stdin, + output = stdout, + error = stderr, + ) { + println("💥Ah-oh!, error: \"$it\"") + StdioClientTransport.StderrSeverity.FATAL + } + + val client = Client( + clientInfo = Implementation( + name = "test-client", + version = "1.0", + ), + ) + + // The error in stderr should cause connecting to fail + // Use explicit timeout to avoid blocking indefinitely due to kotlinx.io cancellation issue (#514) + assertThrows { + withTimeout(5.seconds) { + client.connect(transport) + } + } + + process.destroyForcibly() + } + + @OptIn(ExperimentalAtomicApi::class) + @Test + fun `should start then close cleanly`() = runTest { + // Run loopback process + val processBuilder = createTeeProcessBuilder() + val process = processBuilder.start() + + val input = process.inputStream.asSource().buffered() + val output = process.outputStream.asSink().buffered() + val error = process.errorStream.asSource().buffered() + + val transport = StdioClientTransport( + input = input, + output = output, + error = error, + ) + + transport.onError { error -> + fail("Unexpected error: $error") + } + + val didClose = AtomicBoolean(false) + transport.onClose { didClose.store(true) } + + transport.start() + + // Verify transport stays open after start (use eventually for cross-platform reliability) + eventually(2.seconds) { + assertFalse(didClose.load(), "Transport should not be closed immediately after start") + } + + // Destroy process BEFORE close() to unblock stdin reader + process.destroyForcibly() + + transport.close() + + // Verify transport is closed after close() call + eventually(2.seconds) { + assertTrue(didClose.load(), "Transport should be closed after close() call") + } + } + + @OptIn(ExperimentalAtomicApi::class) + @Test + fun `should close cleanly while stdin read is blocked`(): Unit = runBlocking(Dispatchers.IO) { + val input = BlockingRawSource() + + val transport = StdioClientTransport( + input = input.buffered(), + output = Buffer(), + ) + + val didClose = AtomicBoolean(false) + transport.onClose { didClose.store(true) } + transport.onError { error -> + fail("Unexpected error while closing transport: $error") + } + + transport.start() + + eventually(2.seconds) { + assertTrue(input.readStarted, "Transport should start reading stdin before close") + } + + val closeJob = async { + transport.close() + } + + val closed = withTimeoutOrNull(1.seconds) { + closeJob.await() + } + + input.close() + closeJob.await() + + assertNotNull(closed, "Transport.close() should not wait for stdin to produce data or EOF") + assertTrue(didClose.load(), "Transport should be closed after close() call") + } + + @Test + fun `should read messages`() = runTest { + val processBuilder = createTeeProcessBuilder() + val process = processBuilder.start() + + val input = process.inputStream.asSource().buffered() + val output = process.outputStream.asSink().buffered() + + val transport = StdioClientTransport( + input = input, + output = output, + ) + + testTransportRead(transport) + + process.waitFor() + process.destroyForcibly() + } + + @Test + fun `should ignore first output messages`() = runTest { + val processBuilder = createTeeProcessBuilder() + val process = processBuilder.start() + process.outputStream.write("Stdio server started".toByteArray()) + + val input = process.inputStream.asSource().buffered() + val output = process.outputStream.asSink().buffered() + + val transport = StdioClientTransport( + input = input, + output = output, + ) + + testTransportRead(transport) + + process.waitFor() + process.destroyForcibly() + } + + private class BlockingRawSource : RawSource { + private val closed = CountDownLatch(1) + + @Volatile + var readStarted: Boolean = false + private set + + override fun readAtMostTo(sink: Buffer, byteCount: Long): Long { + readStarted = true + closed.await() + return -1L + } + + override fun close() { + closed.countDown() + } + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/AbstractAuthenticationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/AbstractAuthenticationTest.kt new file mode 100644 index 0000000..ec42041 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/AbstractAuthenticationTest.kt @@ -0,0 +1,187 @@ +package io.modelcontextprotocol.kotlin.sdk.integration + +import io.kotest.matchers.shouldBe +import io.ktor.client.HttpClient +import io.ktor.client.request.basicAuth +import io.ktor.client.request.get +import io.ktor.http.HttpStatusCode +import io.ktor.serialization.kotlinx.json.json +import io.ktor.server.application.Application +import io.ktor.server.application.ApplicationCall +import io.ktor.server.application.install +import io.ktor.server.auth.Authentication +import io.ktor.server.auth.UserIdPrincipal +import io.ktor.server.auth.authenticate +import io.ktor.server.auth.basic +import io.ktor.server.auth.principal +import io.ktor.server.engine.embeddedServer +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation +import io.ktor.server.routing.Route +import io.ktor.server.routing.routing +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import io.modelcontextprotocol.kotlin.sdk.shared.Transport +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.McpJson +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents +import io.modelcontextprotocol.kotlin.test.utils.actualPort +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import kotlin.test.Test +import kotlin.time.Duration.Companion.seconds +import io.ktor.client.engine.cio.CIO as ClientCIO +import io.ktor.server.cio.CIO as ServerCIO +import io.ktor.server.sse.SSE as ServerSSE + +/** + * Base class for MCP authentication integration tests. + */ +abstract class AbstractAuthenticationTest { + + protected companion object { + const val HOST = "127.0.0.1" + const val AUTH_REALM = "mcp-auth" + const val WHOAMI_URI = "whoami://me" + const val VALID_USER = "test-user" + const val VALID_PASSWORD = "valid-password-123" + const val INVALID_USER = "invalid-user" + const val INVALID_PASSWORD = "invalid-password" + } + + /** + * Installs Ktor plugins required by the transport under test. + */ + protected open fun Application.configurePlugins() { + install(ServerSSE) + // ContentNegotiation is required by the StreamableHttp transport for JSON body handling. + // Installing it for SSE tests as well is harmless. + install(ContentNegotiation) { json(McpJson) } + } + + /** + * Registers the MCP server on the given route. + */ + abstract fun Route.registerMcpServer(serverFactory: ApplicationCall.() -> Server) + + /** + * Creates a client transport configured with the given credentials. + */ + abstract fun createClientTransport(baseUrl: String, user: String, pass: String): Transport + + @Test + fun `mcp behind basic auth rejects unauthenticated requests with 401`(): Unit = runBlocking(Dispatchers.IO) { + val server = startAuthenticatedServer() + + val httpClient = HttpClient(ClientCIO) { expectSuccess = false } + try { + httpClient.get("http://$HOST:${server.actualPort()}").status shouldBe HttpStatusCode.Unauthorized + } finally { + httpClient.close() + server.stopSuspend(1000, 2000) + } + } + + @Test + fun `mcp rejects requests with invalid credentials`(): Unit = runBlocking(Dispatchers.IO) { + val server = startAuthenticatedServer() + + val httpClient = HttpClient(ClientCIO) { + expectSuccess = false + } + try { + httpClient.get("http://$HOST:${server.actualPort()}") { + basicAuth(INVALID_USER, INVALID_PASSWORD) + }.status shouldBe HttpStatusCode.Unauthorized + } finally { + httpClient.close() + server.stopSuspend(1000, 2000) + } + } + + @Test + fun `authenticated mcp client can read resource scoped to principal`(): Unit = runBlocking(Dispatchers.IO) { + val server = startAuthenticatedServer() + + val baseUrl = "http://$HOST:${server.actualPort()}" + var mcpClient: Client? = null + try { + mcpClient = Client(Implementation(name = "test-client", version = "1.0.0")) + withTimeout(5.seconds) { + mcpClient.connect(createClientTransport(baseUrl, VALID_USER, VALID_PASSWORD)) + } + + val result = mcpClient.readResource( + ReadResourceRequest(ReadResourceRequestParams(uri = WHOAMI_URI)), + ) + + result.contents shouldBe listOf( + TextResourceContents( + text = VALID_USER, + uri = WHOAMI_URI, + mimeType = "text/plain", + ), + ) + } finally { + mcpClient?.close() + server.stopSuspend(1000, 2000) + } + } + + private suspend fun startAuthenticatedServer() = embeddedServer(ServerCIO, host = HOST, port = 0) { + configurePlugins() + installBasicAuth() + routing { + authenticate(AUTH_REALM) { + registerMcpServer { + createMcpServer { principal()?.name } + } + } + } + }.startSuspend(wait = false) + + private fun Application.installBasicAuth() { + install(Authentication) { + basic(AUTH_REALM) { + validate { credentials -> + if (credentials.name == VALID_USER && credentials.password == VALID_PASSWORD) { + UserIdPrincipal(credentials.name) + } else { + null + } + } + } + } + } + + protected fun createMcpServer(principalProvider: () -> String?): Server = Server( + serverInfo = Implementation(name = "test-server", version = "1.0.0"), + options = ServerOptions( + capabilities = ServerCapabilities( + resources = ServerCapabilities.Resources(), + ), + ), + ).apply { + addResource( + uri = WHOAMI_URI, + name = "Current User", + description = "Returns the name of the authenticated user", + mimeType = "text/plain", + ) { + ReadResourceResult( + contents = listOf( + TextResourceContents( + text = principalProvider() ?: "anonymous", + uri = WHOAMI_URI, + mimeType = "text/plain", + ), + ), + ) + } + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/ProtocolConcurrencyIntegrationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/ProtocolConcurrencyIntegrationTest.kt new file mode 100644 index 0000000..d8d64ec --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/ProtocolConcurrencyIntegrationTest.kt @@ -0,0 +1,306 @@ +package io.modelcontextprotocol.kotlin.sdk.integration + +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import io.modelcontextprotocol.kotlin.sdk.server.ServerSession +import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport +import io.modelcontextprotocol.kotlin.sdk.shared.currentRequestHandlerExtra +import io.modelcontextprotocol.kotlin.sdk.testing.ChannelTransport +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageResult +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.InitializedNotification +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCError +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse +import io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION +import io.modelcontextprotocol.kotlin.sdk.types.ListRootsRequest +import io.modelcontextprotocol.kotlin.sdk.types.ListRootsResult +import io.modelcontextprotocol.kotlin.sdk.types.ListToolsRequest +import io.modelcontextprotocol.kotlin.sdk.types.ListToolsResult +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.RequestId +import io.modelcontextprotocol.kotlin.sdk.types.Role +import io.modelcontextprotocol.kotlin.sdk.types.SamplingMessage +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.toJSON +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.atomic.AtomicReference +import kotlin.coroutines.cancellation.CancellationException +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.time.Duration.Companion.seconds + +/** + * End-to-end tests for concurrent inbound dispatch enabled after the MCP handshake. + * + * Handlers run on a real [kotlinx.coroutines.Dispatchers.Default], so these use [runBlocking] with + * explicit [withTimeout] guards. A virtual test clock would false-fire timeouts against cross-thread + * real dispatchers. + */ +@OptIn(ExperimentalMcpApi::class) +class ProtocolConcurrencyIntegrationTest { + + private fun rootsClient(): Client = Client( + clientInfo = Implementation(name = "test client", version = "1.0"), + options = ClientOptions( + capabilities = ClientCapabilities(roots = ClientCapabilities.Roots(listChanged = null)), + ), + ) + + private fun toolsServer(): Server = Server( + serverInfo = Implementation(name = "test server", version = "1.0"), + options = ServerOptions( + capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = null)), + ), + ) + + // ChannelTransport delivers inbound messages as soon as its read loop starts, which is before + // start() marks it operational. Creating the session first keeps the client's initialize from + // landing on a transport that would reject the session's response. + private suspend fun connect( + client: Client, + clientTransport: ChannelTransport, + server: Server, + serverTransport: ChannelTransport, + ): ServerSession { + val serverSession = server.createSession(serverTransport) + client.connect(clientTransport) + return serverSession + } + + // The motivating deadlock over real serial read loops: a server tool handler makes a + // server-to-client request (roots/list) while still handling the client's tools/call. + @Test + fun `server tool handler can call roots list mid-handling without deadlock`() = runBlocking { + val client = rootsClient() + val server = toolsServer() + val (clientTransport, serverTransport) = ChannelTransport.createLinkedPair() + + val serverSession = connect(client, clientTransport, server, serverTransport) + + // Register the tools/call handler directly on the session so it can call back to the client + // mid-handling. Before concurrent dispatch this would deadlock: the server read loop is + // blocked by the tool handler and can never deliver the roots/list response. + serverSession.setRequestHandler(Method.Defined.ToolsCall) { _, _ -> + val roots = serverSession.request(ListRootsRequest()) + CallToolResult(content = listOf(TextContent(text = "roots=${roots.roots.size}"))) + } + + val result = withTimeout(10.seconds) { + client.callTool(CallToolRequest(CallToolRequestParams("needs-roots"))) + } + val text = (result as CallToolResult).content.filterIsInstance().single().text + text shouldBe "roots=0" + + client.close() + server.close() + } + + // Client-side mirror: a client sampling handler makes a client-to-server request (tools/list) + // while still handling the server's sampling/createMessage. + @Test + fun `client sampling handler can call the server mid-handling without deadlock`() = runBlocking { + val client = Client( + clientInfo = Implementation(name = "test client", version = "1.0"), + options = ClientOptions( + capabilities = ClientCapabilities(sampling = ClientCapabilities.Sampling()), + ), + ) + val server = toolsServer() + val (clientTransport, serverTransport) = ChannelTransport.createLinkedPair() + + client.setRequestHandler(Method.Defined.SamplingCreateMessage) { _, _ -> + val tools = client.request(ListToolsRequest()) + CreateMessageResult( + role = Role.Assistant, + content = TextContent(text = "tools=${tools.tools.size}"), + model = "test-model", + ) + } + + val serverSession = connect(client, clientTransport, server, serverTransport) + + val result = withTimeout(10.seconds) { + serverSession.request( + CreateMessageRequest( + CreateMessageRequestParams( + maxTokens = 100, + messages = listOf(SamplingMessage(role = Role.User, content = TextContent(text = "hi"))), + ), + ), + ) + } + val text = result.content.filterIsInstance().single().text + text shouldBe "tools=0" + + client.close() + server.close() + } + + // Nested-cancel cascade: cancelling the client's tools/call must reach the server tool handler + // over the wire and cascade into its nested roots/list request, cancelling the parked + // client-side roots handler. No tools/call response is ever sent. + @Test + fun `cancelling a tool call cascades into its nested roots list request`() = runBlocking { + val client = rootsClient() + val server = toolsServer() + val (clientTransport, serverTransport) = ChannelTransport.createLinkedPair() + + val capturedToolCallId = AtomicReference(null) + val toolHandlerCancelled = CompletableDeferred() + val rootsHandlerEntered = CompletableDeferred() + val rootsHandlerCancelled = CompletableDeferred() + val parkForever = CompletableDeferred() + val unexpectedToolCallResponse = CompletableDeferred() + + // Tap the server->client direction (client transport inbound) to prove no tools/call + // response is ever delivered once the call is cancelled. + clientTransport.onMessage { message -> + if (message is JSONRPCResponse && message.id == capturedToolCallId.get()) { + unexpectedToolCallResponse.complete(Unit) + } + } + + val serverSession = connect(client, clientTransport, server, serverTransport) + + // Client roots handler parks until cancelled and records the cancellation. + client.setRequestHandler(Method.Defined.RootsList) { _, _ -> + rootsHandlerEntered.complete(Unit) + try { + parkForever.await() + } catch (e: CancellationException) { + rootsHandlerCancelled.complete(e) + throw e + } + } + + // Tool handler makes a nested server->client request and records its own cancellation. + serverSession.setRequestHandler(Method.Defined.ToolsCall) { _, _ -> + val extra = currentRequestHandlerExtra()!! + capturedToolCallId.set(extra.requestId) + try { + extra.sendRequest(ListRootsRequest()) + } catch (e: CancellationException) { + toolHandlerCancelled.complete(e) + throw e + } + CallToolResult(content = listOf(TextContent(text = "unreachable"))) + } + + val call = launch { client.callTool(CallToolRequest(CallToolRequestParams("nested"))) } + + withTimeout(10.seconds) { rootsHandlerEntered.await() } + call.cancel(CancellationException("user aborted")) + + withTimeout(10.seconds) { toolHandlerCancelled.await() } + withTimeout(10.seconds) { rootsHandlerCancelled.await() } + + // The server suppresses the response for a cancelled request, so none is ever sent. + assertFalse( + unexpectedToolCallResponse.isCompleted, + "no tools/call response should be delivered after the call was cancelled", + ) + + client.close() + server.close() + } + + // Init gate plus context element in a high-level handler: initialize and tools/list are + // pipelined before notifications/initialized and answered serially in order. After the + // handshake a high-level addTool handler can observe currentRequestHandlerExtra(). + @Test + fun `pipelined initialize and tools list are processed serially then context element is available`(): Unit = + runBlocking { + val server = toolsServer() + server.addTool(name = "ctx-tool", description = "returns its request id") { + val extra = currentRequestHandlerExtra() + checkNotNull(extra) { "currentRequestHandlerExtra() must be non-null inside a tool handler" } + CallToolResult(content = listOf(TextContent(text = extra.requestId.asText()))) + } + + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + + val arrivalOrder = CopyOnWriteArrayList() + val pending = ConcurrentHashMap>() + clientTransport.onMessage { message -> + val id = when (message) { + is JSONRPCResponse -> message.id + is JSONRPCError -> message.id + else -> null + } + if (id != null) { + arrivalOrder.add(id) + pending[id]?.complete(message) + } + } + + // Drive the client side manually over the raw transport. + server.createSession(serverTransport) + + val initRequest = InitializeRequest( + InitializeRequestParams( + protocolVersion = LATEST_PROTOCOL_VERSION, + capabilities = ClientCapabilities(), + clientInfo = Implementation(name = "raw-client", version = "1.0"), + ), + ).toJSON() + val toolsListRequest = ListToolsRequest().toJSON() + + val initAnswered = pending.expect(initRequest.id) + val toolsListAnswered = pending.expect(toolsListRequest.id) + + // Pipelined before notifications/initialized: both handled in the serial dispatch phase. + clientTransport.send(initRequest) + clientTransport.send(toolsListRequest) + + withTimeout(10.seconds) { initAnswered.await() } + withTimeout(10.seconds) { toolsListAnswered.await() } + + // Deterministic serial ordering: initialize answered before tools/list. + assertEquals( + listOf(initRequest.id, toolsListRequest.id), + arrivalOrder.toList(), + "pipelined requests must be answered in arrival order", + ) + + // Complete the handshake, then call the high-level tool. + clientTransport.send(InitializedNotification().toJSON()) + + val toolCallRequest = CallToolRequest(CallToolRequestParams("ctx-tool")).toJSON() + val toolCallAnswered = pending.expect(toolCallRequest.id) + clientTransport.send(toolCallRequest) + + val toolCallResponse = withTimeout(10.seconds) { toolCallAnswered.await() } as JSONRPCResponse + val text = (toolCallResponse.result as CallToolResult).content.filterIsInstance().single().text + // The handler echoed its request id, proving currentRequestHandlerExtra() was available. + text shouldBe toolCallRequest.id.asText() + } + + private fun ConcurrentHashMap>.expect( + id: RequestId, + ): CompletableDeferred = CompletableDeferred().also { put(id, it) } + + private fun RequestId.asText(): String = when (this) { + is RequestId.StringId -> value + is RequestId.NumberId -> value.toString() + } +} diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractPromptIntegrationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractPromptIntegrationTest.kt similarity index 73% rename from kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractPromptIntegrationTest.kt rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractPromptIntegrationTest.kt index d5644bb..fb97782 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractPromptIntegrationTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractPromptIntegrationTest.kt @@ -1,19 +1,32 @@ package io.modelcontextprotocol.kotlin.sdk.integration.kotlin -import io.modelcontextprotocol.kotlin.sdk.GetPromptRequest -import io.modelcontextprotocol.kotlin.sdk.GetPromptResult -import io.modelcontextprotocol.kotlin.sdk.PromptArgument -import io.modelcontextprotocol.kotlin.sdk.PromptMessage -import io.modelcontextprotocol.kotlin.sdk.Role -import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities -import io.modelcontextprotocol.kotlin.sdk.TextContent +import io.kotest.assertions.withClue +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult +import io.modelcontextprotocol.kotlin.sdk.types.ListPromptsRequest +import io.modelcontextprotocol.kotlin.sdk.types.ListPromptsResult +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.PaginatedRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.Prompt +import io.modelcontextprotocol.kotlin.sdk.types.PromptArgument +import io.modelcontextprotocol.kotlin.sdk.types.PromptMessage +import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import io.modelcontextprotocol.kotlin.sdk.types.Role +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.TextContent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import java.util.concurrent.CopyOnWriteArrayList import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -56,17 +69,17 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { ), ), ) { request -> - val name = request.arguments?.get("name") ?: "World" + val name = request.params.arguments?.get("name") ?: "World" GetPromptResult( description = basicPromptDescription, messages = listOf( PromptMessage( - role = Role.user, + role = Role.User, content = TextContent(text = "Hello, $name!"), ), PromptMessage( - role = Role.assistant, + role = Role.Assistant, content = TextContent(text = "Greetings, $name! How can I assist you today?"), ), ), @@ -85,17 +98,17 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { ), ), ) { request -> - val special = request.arguments?.get("special") ?: specialCharsContent + val special = request.params.arguments?.get("special") ?: specialCharsContent GetPromptResult( description = specialCharsPromptDescription, messages = listOf( PromptMessage( - role = Role.user, + role = Role.User, content = TextContent(text = "Special characters: $special"), ), PromptMessage( - role = Role.assistant, + role = Role.Assistant, content = TextContent(text = "Received special characters: $special"), ), ), @@ -114,18 +127,18 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { ), ), ) { request -> - val size = request.arguments?.get("size")?.toIntOrNull() ?: 1 + val size = request.params.arguments?.get("size")?.toIntOrNull() ?: 1 val content = largePromptContent.repeat(size) GetPromptResult( description = largePromptDescription, messages = listOf( PromptMessage( - role = Role.user, + role = Role.User, content = TextContent(text = "Generate a large response"), ), PromptMessage( - role = Role.assistant, + role = Role.Assistant, content = TextContent(text = content), ), ), @@ -152,7 +165,7 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { // validate required arguments val requiredArgs = listOf("arg1", "arg2", "arg3") for (argName in requiredArgs) { - if (request.arguments?.get(argName) == null) { + if (request.params.arguments?.get(argName) == null) { throw IllegalArgumentException("Missing required argument: $argName") } } @@ -160,7 +173,7 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { val args = mutableMapOf() for (i in 1..10) { val argName = "arg$i" - val argValue = request.arguments?.get(argName) + val argValue = request.params.arguments?.get(argName) if (argValue != null) { args[argName] = argValue } @@ -170,7 +183,7 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { description = complexPromptDescription, messages = listOf( PromptMessage( - role = Role.user, + role = Role.User, content = TextContent( text = "Arguments: ${ args.entries.joinToString { @@ -180,7 +193,7 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { ), ), PromptMessage( - role = Role.assistant, + role = Role.Assistant, content = TextContent(text = "Received ${args.size} arguments"), ), ), @@ -199,38 +212,38 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { ), ), ) { request -> - val topic = request.arguments?.get("topic") ?: "weather" + val topic = request.params.arguments?.get("topic") ?: "weather" GetPromptResult( description = conversationPromptDescription, messages = listOf( PromptMessage( - role = Role.user, + role = Role.User, content = TextContent(text = "Let's talk about the $topic."), ), PromptMessage( - role = Role.assistant, + role = Role.Assistant, content = TextContent( text = "Sure, I'd love to discuss the $topic. What would you like to know?", ), ), PromptMessage( - role = Role.user, + role = Role.User, content = TextContent(text = "What's your opinion on the $topic?"), ), PromptMessage( - role = Role.assistant, + role = Role.Assistant, content = TextContent( text = "As an AI, I don't have personal opinions," + " but I can provide information about $topic.", ), ), PromptMessage( - role = Role.user, + role = Role.User, content = TextContent(text = "That's helpful, thank you!"), ), PromptMessage( - role = Role.assistant, + role = Role.Assistant, content = TextContent( text = "You're welcome! Let me know if you have more questions about $topic.", ), @@ -261,7 +274,7 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { ), ), ) { request -> - val args = request.arguments ?: emptyMap() + val args = request.params.arguments.orEmpty() val arg1 = args["requiredArg1"] ?: throw IllegalArgumentException( "Missing required argument: requiredArg1", ) @@ -274,11 +287,11 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { description = strictPromptDescription, messages = listOf( PromptMessage( - role = Role.user, + role = Role.User, content = TextContent(text = "Required arguments: $arg1, $arg2. Optional: $optArg"), ), PromptMessage( - role = Role.assistant, + role = Role.Assistant, content = TextContent(text = "I received your arguments: $arg1, $arg2, and $optArg"), ), ), @@ -319,8 +332,10 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { val testName = "Alice" val result = client.getPrompt( GetPromptRequest( - name = basicPromptName, - arguments = mapOf("name" to testName), + GetPromptRequestParams( + name = basicPromptName, + arguments = mapOf("name" to testName), + ), ), ) @@ -334,7 +349,7 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { assertTrue(result.messages.isNotEmpty(), "Prompt messages should not be empty") assertEquals(2, result.messages.size, "Prompt should have 2 messages") - val userMessage = result.messages.find { it.role == Role.user } + val userMessage = result.messages.find { it.role == Role.User } assertNotNull(userMessage, "User message should be in the list") val userContent = userMessage.content as? TextContent assertNotNull(userContent, "User message content should be TextContent") @@ -345,7 +360,7 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { "User message content should match", ) - val assistantMessage = result.messages.find { it.role == Role.assistant } + val assistantMessage = result.messages.find { it.role == Role.Assistant } assertNotNull(assistantMessage, "Assistant message should be in the list") val assistantContent = assistantMessage.content as? TextContent assertNotNull(assistantContent, "Assistant message content should be TextContent") @@ -373,48 +388,50 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { ) // test missing required arg - val exception = assertThrows { + val exception = assertThrows { runBlocking { client.getPrompt( GetPromptRequest( - name = strictPromptName, - arguments = mapOf("requiredArg1" to "value1"), + GetPromptRequestParams( + name = strictPromptName, + arguments = mapOf("requiredArg1" to "value1"), + ), ), ) } } - assertEquals( - true, - exception.message?.contains("requiredArg2"), - "Exception should mention the missing argument", - ) + withClue("Exception should mention the missing argument") { + exception.message shouldContain "requiredArg2" + } // test with no args - val exception2 = assertThrows { + val exception2 = assertThrows { runBlocking { client.getPrompt( GetPromptRequest( - name = strictPromptName, - arguments = emptyMap(), + GetPromptRequestParams( + name = strictPromptName, + arguments = emptyMap(), + ), ), ) } } - assertEquals( - exception2.message?.contains("requiredArg"), - true, - "Exception should mention a missing required argument", - ) + withClue("Exception should mention a missing required argument") { + exception2.message shouldContain "requiredArg" + } // test with all required args val result = client.getPrompt( GetPromptRequest( - name = strictPromptName, - arguments = mapOf( - "requiredArg1" to "value1", - "requiredArg2" to "value2", + GetPromptRequestParams( + name = strictPromptName, + arguments = mapOf( + "requiredArg1" to "value1", + "requiredArg2" to "value2", + ), ), ), ) @@ -422,7 +439,7 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { assertNotNull(result, "Get prompt result should not be null") assertEquals(2, result.messages.size, "Prompt should have 2 messages") - val userMessage = result.messages.find { it.role == Role.user } + val userMessage = result.messages.find { it.role == Role.User } assertNotNull(userMessage, "User message should be in the list") val userContent = userMessage.content as? TextContent assertNotNull(userContent, "User message content should be TextContent") @@ -436,8 +453,10 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { val topic = "climate change" val result = client.getPrompt( GetPromptRequest( - name = conversationPromptName, - arguments = mapOf("topic" to topic), + GetPromptRequestParams( + name = conversationPromptName, + arguments = mapOf("topic" to topic), + ), ), ) @@ -451,14 +470,14 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { assertTrue(result.messages.isNotEmpty(), "Prompt messages should not be empty") assertEquals(6, result.messages.size, "Prompt should have 6 messages") - val userMessages = result.messages.filter { it.role == Role.user } - val assistantMessages = result.messages.filter { it.role == Role.assistant } + val userMessages = result.messages.filter { it.role == Role.User } + val assistantMessages = result.messages.filter { it.role == Role.Assistant } assertEquals(3, userMessages.size, "Should have 3 user messages") assertEquals(3, assistantMessages.size, "Should have 3 assistant messages") for (i in 0 until result.messages.size) { - val expectedRole = if (i % 2 == 0) Role.user else Role.assistant + val expectedRole = if (i % 2 == 0) Role.User else Role.Assistant assertEquals( expectedRole, result.messages[i].role, @@ -487,8 +506,10 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { val testName = "Alice" val result = client.getPrompt( GetPromptRequest( - name = basicPromptName, - arguments = mapOf("name" to testName), + GetPromptRequestParams( + name = basicPromptName, + arguments = mapOf("name" to testName), + ), ), ) @@ -497,13 +518,13 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { assertEquals(2, result.messages.size, "Prompt should have 2 messages") - val userMessage = result.messages.find { it.role == Role.user } + val userMessage = result.messages.find { it.role == Role.User } assertNotNull(userMessage, "User message should be in the list") val userContent = userMessage.content as? TextContent assertNotNull(userContent, "User message content should be TextContent") assertEquals("Hello, $testName!", userContent.text, "User message content should match") - val assistantMessage = result.messages.find { it.role == Role.assistant } + val assistantMessage = result.messages.find { it.role == Role.Assistant } assertNotNull(assistantMessage, "Assistant message should be in the list") val assistantContent = assistantMessage.content as? TextContent assertNotNull(assistantContent, "Assistant message content should be TextContent") @@ -520,8 +541,10 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { val result = client.getPrompt( GetPromptRequest( - name = complexPromptName, - arguments = arguments, + GetPromptRequestParams( + name = complexPromptName, + arguments = arguments, + ), ), ) @@ -530,18 +553,18 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { assertEquals(2, result.messages.size, "Prompt should have 2 messages") - val userMessage = result.messages.find { it.role == Role.user } + val userMessage = result.messages.find { it.role == Role.User } assertNotNull(userMessage, "User message should be in the list") val userContent = userMessage.content as? TextContent assertNotNull(userContent, "User message content should be TextContent") // verify all arguments - val text = userContent.text ?: "" + val text = userContent.text for (i in 1..10) { assertTrue(text.contains("arg$i=value$i"), "Message should contain arg$i=value$i") } - val assistantMessage = result.messages.find { it.role == Role.assistant } + val assistantMessage = result.messages.find { it.role == Role.Assistant } assertNotNull(assistantMessage, "Assistant message should be in the list") val assistantContent = assistantMessage.content as? TextContent assertNotNull(assistantContent, "Assistant message content should be TextContent") @@ -556,8 +579,10 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { fun testLargePrompt() = runBlocking(Dispatchers.IO) { val result = client.getPrompt( GetPromptRequest( - name = largePromptName, - arguments = mapOf("size" to "1"), + GetPromptRequestParams( + name = largePromptName, + arguments = mapOf("size" to "1"), + ), ), ) @@ -566,11 +591,11 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { assertEquals(2, result.messages.size, "Prompt should have 2 messages") - val assistantMessage = result.messages.find { it.role == Role.assistant } + val assistantMessage = result.messages.find { it.role == Role.Assistant } assertNotNull(assistantMessage, "Assistant message should be in the list") val assistantContent = assistantMessage.content as? TextContent assertNotNull(assistantContent, "Assistant message content should be TextContent") - val text = assistantContent.text ?: "" + val text = assistantContent.text assertEquals(100_000, text.length, "Assistant message should be 100KB in size") } @@ -578,8 +603,10 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { fun testSpecialCharacters() = runBlocking(Dispatchers.IO) { val result = client.getPrompt( GetPromptRequest( - name = specialCharsPromptName, - arguments = mapOf("special" to specialCharsContent), + GetPromptRequestParams( + name = specialCharsPromptName, + arguments = mapOf("special" to specialCharsContent), + ), ), ) @@ -588,18 +615,18 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { assertEquals(2, result.messages.size, "Prompt should have 2 messages") - val userMessage = result.messages.find { it.role == Role.user } + val userMessage = result.messages.find { it.role == Role.User } assertNotNull(userMessage, "User message should be in the list") val userContent = userMessage.content as? TextContent assertNotNull(userContent, "User message content should be TextContent") - val userText = userContent.text ?: "" + val userText = userContent.text assertTrue(userText.contains(specialCharsContent), "User message should contain special characters") - val assistantMessage = result.messages.find { it.role == Role.assistant } + val assistantMessage = result.messages.find { it.role == Role.Assistant } assertNotNull(assistantMessage, "Assistant message should be in the list") val assistantContent = assistantMessage.content as? TextContent assertNotNull(assistantContent, "Assistant message content should be TextContent") - val assistantText = assistantContent.text ?: "" + val assistantText = assistantContent.text assertTrue( assistantText.contains(specialCharsContent), "Assistant message should contain special characters", @@ -630,8 +657,10 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { val result = client.getPrompt( GetPromptRequest( - name = promptName, - arguments = arguments, + GetPromptRequestParams( + name = promptName, + arguments = arguments, + ), ), ) @@ -654,20 +683,91 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { fun testNonExistentPrompt() = runTest { val nonExistentPromptName = "non-existent-prompt" - val exception = assertThrows { + val exception = assertThrows { runBlocking { client.getPrompt( GetPromptRequest( - name = nonExistentPromptName, - arguments = mapOf("name" to "Test"), + GetPromptRequestParams( + name = nonExistentPromptName, + arguments = mapOf("name" to "Test"), + ), ), ) } } - val msg = exception.message ?: "" - val expectedMessage = "JSONRPCError(code=InternalError, message=Prompt not found: non-existent-prompt, data={})" + val expectedMessage = "Prompt not found: non-existent-prompt" - assertEquals(expectedMessage, msg, "Unexpected error message for non-existent prompt") + withClue("Exception code should be INVALID_PARAMS: ${RPCError.ErrorCode.INVALID_PARAMS}") { + exception.code shouldBe RPCError.ErrorCode.INVALID_PARAMS + } + withClue("Unexpected error message for non-existent prompt") { + exception.message shouldBe expectedMessage + } + } + + @Test + fun testListPromptsPagination() = runBlocking(Dispatchers.IO) { + val receivedCursors = CopyOnWriteArrayList() + val page1 = listOf(Prompt(name = "p-1", description = "desc"), Prompt(name = "p-2", description = "desc")) + val page2 = listOf(Prompt(name = "p-3", description = "desc"), Prompt(name = "p-4", description = "desc")) + val page3 = listOf(Prompt(name = "p-5", description = "desc")) + + server.sessions.forEach { (_, session) -> + session.setRequestHandler(Method.Defined.PromptsList) { request, _ -> + receivedCursors += request.cursor + when (request.cursor) { + null -> ListPromptsResult(prompts = page1, nextCursor = "cursor-2") + "cursor-2" -> ListPromptsResult(prompts = page2, nextCursor = "cursor-3") + "cursor-3" -> ListPromptsResult(prompts = page3, nextCursor = null) + else -> error("Unexpected cursor: ${request.cursor}") + } + } + } + + val collected = mutableListOf() + var cursor: String? = null + do { + val request = if (cursor == null) { + ListPromptsRequest() + } else { + ListPromptsRequest(PaginatedRequestParams(cursor = cursor)) + } + val response = client.listPrompts(request) + collected += response.prompts + cursor = response.nextCursor + } while (cursor != null) + + assertEquals( + listOf(null, "cursor-2", "cursor-3"), + receivedCursors.toList(), + "Client must forward each nextCursor into the next request", + ) + assertEquals( + listOf("p-1", "p-2", "p-3", "p-4", "p-5"), + collected.map { it.name }, + "Client must accumulate pages in order without duplicates or reordering", + ) + } + + @Test + fun testListPromptsInvalidCursor() = runBlocking(Dispatchers.IO) { + val invalidCursor = "not-a-valid-cursor" + + server.sessions.forEach { (_, session) -> + session.setRequestHandler(Method.Defined.PromptsList) { _, _ -> + throw McpException( + code = RPCError.ErrorCode.INVALID_PARAMS, + message = "Invalid cursor: $invalidCursor", + ) + } + } + + val exception = assertFailsWith { + client.listPrompts(ListPromptsRequest(PaginatedRequestParams(cursor = invalidCursor))) + } + + assertEquals(RPCError.ErrorCode.INVALID_PARAMS, exception.code) + assertEquals("Invalid cursor: $invalidCursor", exception.message) } } diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractResourceIntegrationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractResourceIntegrationTest.kt similarity index 63% rename from kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractResourceIntegrationTest.kt rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractResourceIntegrationTest.kt index 95c643a..ab2a854 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractResourceIntegrationTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractResourceIntegrationTest.kt @@ -1,22 +1,32 @@ package io.modelcontextprotocol.kotlin.sdk.integration.kotlin -import io.modelcontextprotocol.kotlin.sdk.BlobResourceContents -import io.modelcontextprotocol.kotlin.sdk.EmptyRequestResult -import io.modelcontextprotocol.kotlin.sdk.Method -import io.modelcontextprotocol.kotlin.sdk.ReadResourceRequest -import io.modelcontextprotocol.kotlin.sdk.ReadResourceResult -import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities -import io.modelcontextprotocol.kotlin.sdk.SubscribeRequest -import io.modelcontextprotocol.kotlin.sdk.TextResourceContents -import io.modelcontextprotocol.kotlin.sdk.UnsubscribeRequest +import io.modelcontextprotocol.kotlin.sdk.types.BlobResourceContents +import io.modelcontextprotocol.kotlin.sdk.types.ListResourcesRequest +import io.modelcontextprotocol.kotlin.sdk.types.ListResourcesResult +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.PaginatedRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult +import io.modelcontextprotocol.kotlin.sdk.types.Resource +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.SubscribeRequest +import io.modelcontextprotocol.kotlin.sdk.types.SubscribeRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents +import io.modelcontextprotocol.kotlin.sdk.types.UnsubscribeRequest +import io.modelcontextprotocol.kotlin.sdk.types.UnsubscribeRequestParams import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.atomic.AtomicBoolean import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -60,32 +70,7 @@ abstract class AbstractResourceIntegrationTest : KotlinTestBase() { contents = listOf( TextResourceContents( text = testResourceContent, - uri = request.uri, - mimeType = "text/plain", - ), - ), - ) - } - - server.setRequestHandler(Method.Defined.ResourcesSubscribe) { _, _ -> - EmptyRequestResult() - } - - server.setRequestHandler(Method.Defined.ResourcesUnsubscribe) { _, _ -> - EmptyRequestResult() - } - - server.addResource( - uri = testResourceUri, - name = testResourceName, - description = testResourceDescription, - mimeType = "text/plain", - ) { request -> - ReadResourceResult( - contents = listOf( - TextResourceContents( - text = testResourceContent, - uri = request.uri, + uri = request.params.uri, mimeType = "text/plain", ), ), @@ -102,7 +87,7 @@ abstract class AbstractResourceIntegrationTest : KotlinTestBase() { contents = listOf( BlobResourceContents( blob = binaryResourceContent, - uri = request.uri, + uri = request.params.uri, mimeType = "image/png", ), ), @@ -119,7 +104,7 @@ abstract class AbstractResourceIntegrationTest : KotlinTestBase() { contents = listOf( TextResourceContents( text = largeResourceContent, - uri = request.uri, + uri = request.params.uri, mimeType = "text/plain", ), ), @@ -136,7 +121,7 @@ abstract class AbstractResourceIntegrationTest : KotlinTestBase() { contents = listOf( TextResourceContents( text = if (dynamicResourceContent.get()) "Updated content" else "Original content", - uri = request.uri, + uri = request.params.uri, mimeType = "text/plain", ), ), @@ -159,7 +144,7 @@ abstract class AbstractResourceIntegrationTest : KotlinTestBase() { @Test fun testReadResource() = runBlocking(Dispatchers.IO) { - val result = client.readResource(ReadResourceRequest(uri = testResourceUri)) + val result = client.readResource(ReadResourceRequest(ReadResourceRequestParams(uri = testResourceUri))) assertNotNull(result, "Read resource result should not be null") assertTrue(result.contents.isNotEmpty(), "Resource contents should not be empty") @@ -172,17 +157,19 @@ abstract class AbstractResourceIntegrationTest : KotlinTestBase() { @Test fun testSubscribeAndUnsubscribe() { runBlocking(Dispatchers.IO) { - val subscribeResult = client.subscribeResource(SubscribeRequest(uri = testResourceUri)) + val subscribeResult = + client.subscribeResource(SubscribeRequest(SubscribeRequestParams(uri = testResourceUri))) assertNotNull(subscribeResult, "Subscribe result should not be null") - val unsubscribeResult = client.unsubscribeResource(UnsubscribeRequest(uri = testResourceUri)) + val unsubscribeResult = + client.unsubscribeResource(UnsubscribeRequest(UnsubscribeRequestParams(uri = testResourceUri))) assertNotNull(unsubscribeResult, "Unsubscribe result should not be null") } } @Test fun testBinaryResource() = runBlocking(Dispatchers.IO) { - val result = client.readResource(ReadResourceRequest(uri = binaryResourceUri)) + val result = client.readResource(ReadResourceRequest(ReadResourceRequestParams(uri = binaryResourceUri))) assertNotNull(result, "Read resource result should not be null") assertTrue(result.contents.isNotEmpty(), "Resource contents should not be empty") @@ -195,7 +182,7 @@ abstract class AbstractResourceIntegrationTest : KotlinTestBase() { @Test fun testLargeResource() = runBlocking(Dispatchers.IO) { - val result = client.readResource(ReadResourceRequest(uri = largeResourceUri)) + val result = client.readResource(ReadResourceRequest(ReadResourceRequestParams(uri = largeResourceUri))) assertNotNull(result, "Read resource result should not be null") assertTrue(result.contents.isNotEmpty(), "Resource contents should not be empty") @@ -210,22 +197,23 @@ abstract class AbstractResourceIntegrationTest : KotlinTestBase() { fun testInvalidResourceUri() = runTest { val invalidUri = "test://nonexistent.txt" - val exception = assertThrows { + val exception = assertThrows { runBlocking { - client.readResource(ReadResourceRequest(uri = invalidUri)) + client.readResource(ReadResourceRequest(ReadResourceRequestParams(uri = invalidUri))) } } - val msg = exception.message ?: "" - val expectedMessage = - "JSONRPCError(code=InternalError, message=Resource not found: test://nonexistent.txt, data={})" - - assertEquals(expectedMessage, msg, "Unexpected error message for invalid resource URI") + assertEquals( + RPCError.ErrorCode.RESOURCE_NOT_FOUND, + exception.code, + "Exception code should be RESOURCE_NOT_FOUND: ${RPCError.ErrorCode.RESOURCE_NOT_FOUND}", + ) } @Test fun testDynamicResource() = runBlocking(Dispatchers.IO) { - val initialResult = client.readResource(ReadResourceRequest(uri = dynamicResourceUri)) + val initialResult = + client.readResource(ReadResourceRequest(ReadResourceRequestParams(uri = dynamicResourceUri))) assertNotNull(initialResult, "Initial read result should not be null") val initialContent = (initialResult.contents.firstOrNull() as? TextResourceContents)?.text assertEquals("Original content", initialContent, "Initial content should match") @@ -233,7 +221,8 @@ abstract class AbstractResourceIntegrationTest : KotlinTestBase() { // update resource dynamicResourceContent.set(true) - val updatedResult = client.readResource(ReadResourceRequest(uri = dynamicResourceUri)) + val updatedResult = + client.readResource(ReadResourceRequest(ReadResourceRequestParams(uri = dynamicResourceUri))) assertNotNull(updatedResult, "Updated read result should not be null") val updatedContent = (updatedResult.contents.firstOrNull() as? TextResourceContents)?.text assertEquals("Updated content", updatedContent, "Updated content should match") @@ -256,7 +245,7 @@ abstract class AbstractResourceIntegrationTest : KotlinTestBase() { contents = listOf( TextResourceContents( text = "New resource content", - uri = request.uri, + uri = request.params.uri, mimeType = "text/plain", ), ), @@ -296,7 +285,7 @@ abstract class AbstractResourceIntegrationTest : KotlinTestBase() { else -> largeResourceUri } - val result = client.readResource(ReadResourceRequest(uri = uri)) + val result = client.readResource(ReadResourceRequest(ReadResourceRequestParams(uri = uri))) synchronized(results) { results.add(result) } @@ -310,4 +299,76 @@ abstract class AbstractResourceIntegrationTest : KotlinTestBase() { assertTrue(result.contents.isNotEmpty(), "Result contents should not be empty") } } + + @Test + fun testListResourcesPagination() = runBlocking(Dispatchers.IO) { + val receivedCursors = CopyOnWriteArrayList() + val page1 = listOf( + Resource(uri = "test://r-1", name = "r-1"), + Resource(uri = "test://r-2", name = "r-2"), + Resource(uri = "test://r-3", name = "r-3"), + ) + val page2 = listOf( + Resource(uri = "test://r-4", name = "r-4"), + Resource(uri = "test://r-5", name = "r-5"), + ) + val page3 = listOf(Resource(uri = "test://r-6", name = "r-6")) + + server.sessions.forEach { (_, session) -> + session.setRequestHandler(Method.Defined.ResourcesList) { request, _ -> + receivedCursors += request.cursor + when (request.cursor) { + null -> ListResourcesResult(resources = page1, nextCursor = "cursor-2") + "cursor-2" -> ListResourcesResult(resources = page2, nextCursor = "cursor-3") + "cursor-3" -> ListResourcesResult(resources = page3, nextCursor = null) + else -> error("Unexpected cursor: ${request.cursor}") + } + } + } + + val collected = mutableListOf() + var cursor: String? = null + do { + val request = if (cursor == null) { + ListResourcesRequest() + } else { + ListResourcesRequest(PaginatedRequestParams(cursor = cursor)) + } + val response = client.listResources(request) + collected += response.resources + cursor = response.nextCursor + } while (cursor != null) + + assertEquals( + listOf(null, "cursor-2", "cursor-3"), + receivedCursors.toList(), + "Client must forward each nextCursor into the next request", + ) + assertEquals( + listOf("test://r-1", "test://r-2", "test://r-3", "test://r-4", "test://r-5", "test://r-6"), + collected.map { it.uri }, + "Client must accumulate pages in order without duplicates or reordering", + ) + } + + @Test + fun testListResourcesInvalidCursor() = runBlocking(Dispatchers.IO) { + val invalidCursor = "not-a-valid-cursor" + + server.sessions.forEach { (_, session) -> + session.setRequestHandler(Method.Defined.ResourcesList) { _, _ -> + throw McpException( + code = RPCError.ErrorCode.INVALID_PARAMS, + message = "Invalid cursor: $invalidCursor", + ) + } + } + + val exception = assertFailsWith { + client.listResources(ListResourcesRequest(PaginatedRequestParams(cursor = invalidCursor))) + } + + assertEquals(RPCError.ErrorCode.INVALID_PARAMS, exception.code) + assertEquals("Invalid cursor: $invalidCursor", exception.message) + } } diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractToolIntegrationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractToolIntegrationTest.kt similarity index 76% rename from kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractToolIntegrationTest.kt rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractToolIntegrationTest.kt index 3b0de29..b802122 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractToolIntegrationTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractToolIntegrationTest.kt @@ -1,14 +1,21 @@ package io.modelcontextprotocol.kotlin.sdk.integration.kotlin import io.kotest.assertions.json.shouldEqualJson -import io.modelcontextprotocol.kotlin.sdk.CallToolRequest -import io.modelcontextprotocol.kotlin.sdk.CallToolResult -import io.modelcontextprotocol.kotlin.sdk.CallToolResultBase -import io.modelcontextprotocol.kotlin.sdk.ImageContent -import io.modelcontextprotocol.kotlin.sdk.PromptMessageContent -import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities -import io.modelcontextprotocol.kotlin.sdk.TextContent -import io.modelcontextprotocol.kotlin.sdk.Tool +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.ContentBlock +import io.modelcontextprotocol.kotlin.sdk.types.ImageContent +import io.modelcontextprotocol.kotlin.sdk.types.ListToolsRequest +import io.modelcontextprotocol.kotlin.sdk.types.ListToolsResult +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.PaginatedRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.Tool +import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -21,11 +28,12 @@ import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows import java.text.DecimalFormat import java.text.DecimalFormatSymbols import java.util.Locale +import java.util.concurrent.CopyOnWriteArrayList import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -70,7 +78,7 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { server.addTool( name = testToolName, description = testToolDescription, - inputSchema = Tool.Input( + inputSchema = ToolSchema( properties = buildJsonObject { put( "text", @@ -83,7 +91,7 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { required = listOf("text"), ), ) { request -> - val text = (request.arguments["text"] as? JsonPrimitive)?.content ?: "No text provided" + val text = (request.params.arguments?.get("text") as? JsonPrimitive)?.content ?: "No text provided" CallToolResult( content = listOf(TextContent(text = "Echo: $text")), @@ -98,7 +106,7 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { server.addTool( name = basicToolName, description = basicToolDescription, - inputSchema = Tool.Input( + inputSchema = ToolSchema( properties = buildJsonObject { put( "text", @@ -111,7 +119,7 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { required = listOf("text"), ), ) { request -> - val text = (request.arguments["text"] as? JsonPrimitive)?.content ?: "No text provided" + val text = (request.params.arguments?.get("text") as? JsonPrimitive)?.content ?: "No text provided" CallToolResult( content = listOf(TextContent(text = "Echo: $text")), @@ -124,7 +132,7 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { server.addTool( name = specialCharsToolName, description = specialCharsToolDescription, - inputSchema = Tool.Input( + inputSchema = ToolSchema( properties = buildJsonObject { put( "special", @@ -136,7 +144,7 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { }, ), ) { request -> - val special = (request.arguments["special"] as? JsonPrimitive)?.content ?: specialCharsContent + val special = (request.params.arguments?.get("special") as? JsonPrimitive)?.content ?: specialCharsContent CallToolResult( content = listOf(TextContent(text = "Received special characters: $special")), @@ -150,7 +158,7 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { server.addTool( name = slowToolName, description = slowToolDescription, - inputSchema = Tool.Input( + inputSchema = ToolSchema( properties = buildJsonObject { put( "delay", @@ -162,7 +170,7 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { }, ), ) { request -> - val delay = (request.arguments["delay"] as? JsonPrimitive)?.content?.toIntOrNull() ?: 1000 + val delay = (request.params.arguments?.get("delay") as? JsonPrimitive)?.content?.toIntOrNull() ?: 1000 // simulate slow operation runBlocking { @@ -180,7 +188,7 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { server.addTool( name = largeToolName, description = largeToolDescription, - inputSchema = Tool.Input( + inputSchema = ToolSchema( properties = buildJsonObject { put( "size", @@ -192,7 +200,7 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { }, ), ) { request -> - val size = (request.arguments["size"] as? JsonPrimitive)?.content?.toIntOrNull() ?: 1 + val size = (request.params.arguments?.get("size") as? JsonPrimitive)?.content?.toIntOrNull() ?: 1 val content = largeToolContent.take(largeToolContent.length.coerceAtMost(size * 1000)) CallToolResult( @@ -206,7 +214,7 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { server.addTool( name = complexToolName, description = complexToolDescription, - inputSchema = Tool.Input( + inputSchema = ToolSchema( properties = buildJsonObject { put( "operation", @@ -271,12 +279,13 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { required = listOf("operation", "a", "b"), ), ) { request -> - val operation = (request.arguments["operation"] as? JsonPrimitive)?.content ?: "add" - val a = (request.arguments["a"] as? JsonPrimitive)?.content?.toDoubleOrNull() ?: 0.0 - val b = (request.arguments["b"] as? JsonPrimitive)?.content?.toDoubleOrNull() ?: 0.0 - val precision = (request.arguments["precision"] as? JsonPrimitive)?.content?.toIntOrNull() ?: 2 - val showSteps = (request.arguments["showSteps"] as? JsonPrimitive)?.content?.toBoolean() ?: false - val tags = (request.arguments["tags"] as? JsonArray)?.mapNotNull { + val operation = (request.params.arguments?.get("operation") as? JsonPrimitive)?.content ?: "add" + val a = (request.params.arguments?.get("a") as? JsonPrimitive)?.content?.toDoubleOrNull() ?: 0.0 + val b = (request.params.arguments?.get("b") as? JsonPrimitive)?.content?.toDoubleOrNull() ?: 0.0 + val precision = (request.params.arguments?.get("precision") as? JsonPrimitive)?.content?.toIntOrNull() ?: 2 + val showSteps = + (request.params.arguments?.get("showSteps") as? JsonPrimitive)?.content?.toBoolean() ?: false + val tags = (request.params.arguments?.get("tags") as? JsonArray)?.mapNotNull { (it as? JsonPrimitive)?.content } ?: emptyList() @@ -289,7 +298,7 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { } val pattern = if (precision > 0) "0." + "0".repeat(precision) else "0" - val symbols = DecimalFormatSymbols(Locale.US).apply { decimalSeparator = ',' } + val symbols = DecimalFormatSymbols(Locale.US).apply { decimalSeparator = '.' } val df = DecimalFormat(pattern, symbols).apply { isGroupingUsed = false } val formattedResult = df.format(result) @@ -320,7 +329,7 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { server.addTool( name = errorToolName, description = errorToolDescription, - inputSchema = Tool.Input( + inputSchema = ToolSchema( properties = buildJsonObject { put( "errorType", @@ -349,8 +358,8 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { required = listOf("errorType"), ), ) { request -> - val errorType = (request.arguments["errorType"] as? JsonPrimitive)?.content ?: "none" - val message = (request.arguments["message"] as? JsonPrimitive)?.content ?: "An error occurred" + val errorType = (request.params.arguments?.get("errorType") as? JsonPrimitive)?.content ?: "none" + val message = (request.params.arguments?.get("message") as? JsonPrimitive)?.content ?: "An error occurred" when (errorType) { "exception" -> throw IllegalArgumentException(message) @@ -378,7 +387,7 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { server.addTool( name = multiContentToolName, description = multiContentToolDescription, - inputSchema = Tool.Input( + inputSchema = ToolSchema( properties = buildJsonObject { put( "text", @@ -399,10 +408,11 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { required = listOf("text"), ), ) { request -> - val text = (request.arguments["text"] as? JsonPrimitive)?.content ?: "Default text" - val includeImage = (request.arguments["includeImage"] as? JsonPrimitive)?.content?.toBoolean() ?: true + val text = (request.params.arguments?.get("text") as? JsonPrimitive)?.content ?: "Default text" + val includeImage = + (request.params.arguments?.get("includeImage") as? JsonPrimitive)?.content?.toBoolean() ?: true - val content = mutableListOf( + val content = mutableListOf( TextContent(text = "Text content: $text"), ) @@ -447,7 +457,7 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { val testText = "Hello, world!" val arguments = mapOf("text" to testText) - val result = client.callTool(testToolName, arguments) as CallToolResultBase + val result = client.callTool(testToolName, arguments) val actualContent = result.structuredContent.toString() val expectedContent = """ @@ -473,7 +483,7 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { "tags" to listOf("test", "calculator", "integration"), ) - val result = client.callTool(complexToolName, arguments) as CallToolResultBase + val result = client.callTool(complexToolName, arguments) val actualContent = result.structuredContent.toString() val expectedContent = """ @@ -482,9 +492,9 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { "a" : 5.5, "b" : 2.0, "result" : 11.0, - "formattedResult" : "11,000", + "formattedResult" : "11.000", "precision" : 3, - "tags" : [ ] + "tags" : ["test", "calculator", "integration"] } """.trimIndent() @@ -496,7 +506,7 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { val successArgs = mapOf("errorType" to "none") val successResult = client.callTool(errorToolName, successArgs) - val actualContent = successResult?.structuredContent.toString() + val actualContent = successResult.structuredContent.toString() val expectedContent = """ { "error" : false, @@ -510,7 +520,7 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { "errorType" to "error", "message" to "Custom error message", ) - val errorResult = client.callTool(errorToolName, errorArgs) as CallToolResultBase + val errorResult = client.callTool(errorToolName, errorArgs) val actualError = errorResult.structuredContent.toString() val expectedError = """ @@ -527,16 +537,18 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { "message" to "My exception message", ) - val exception = assertThrows { - runBlocking { - client.callTool(errorToolName, exceptionArgs) - } - } + val exceptionResult = client.callTool(errorToolName, exceptionArgs) - val msg = exception.message ?: "" - val expectedMessage = "JSONRPCError(code=InternalError, message=My exception message, data={})" + assertTrue(exceptionResult.isError ?: false, "isError should be true for exception") - assertEquals(expectedMessage, msg, "Unexpected error message for exception") + val exceptionContent = exceptionResult.content.firstOrNull { it is TextContent } as? TextContent + assertNotNull(exceptionContent, "Error content should be present in the result") + + val exceptionText = exceptionContent.text + assertTrue( + exceptionText.contains("Error executing tool") && exceptionText.contains("My exception message"), + "Error message should contain the exception details", + ) } @Test @@ -547,7 +559,7 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { "includeImage" to true, ) - val result = client.callTool(multiContentToolName, arguments) as CallToolResultBase + val result = client.callTool(multiContentToolName, arguments) assertEquals( 2, @@ -584,7 +596,7 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { "includeImage" to false, ) - val textOnlyResult = client.callTool(multiContentToolName, textOnlyArgs) as CallToolResultBase + val textOnlyResult = client.callTool(multiContentToolName, textOnlyArgs) assertEquals( 1, @@ -621,10 +633,12 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { val result = client.callTool( CallToolRequest( - name = complexToolName, - arguments = arguments, + CallToolRequestParams( + name = complexToolName, + arguments = arguments, + ), ), - ) as CallToolResultBase + ) val actualContent = result.structuredContent.toString() val expectedContent = """ @@ -633,7 +647,7 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { "a": 0.0, "b": 0.0, "result": 0.0, - "formattedResult": "0,00", + "formattedResult": "0.00", "precision": 2, "tags": [] } @@ -647,7 +661,7 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { val size = 10 val arguments = mapOf("size" to size) - val result = client.callTool(largeToolName, arguments) as CallToolResultBase + val result = client.callTool(largeToolName, arguments) val content = result.content.firstOrNull() as TextContent assertNotNull(content, "Tool result content should be TextContent") @@ -668,7 +682,7 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { val arguments = mapOf("delay" to delay) val startTime = System.currentTimeMillis() - val result = client.callTool(slowToolName, arguments) as CallToolResultBase + val result = client.callTool(slowToolName, arguments) val endTime = System.currentTimeMillis() val content = result.content.firstOrNull() as? TextContent @@ -691,11 +705,11 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { runBlocking(Dispatchers.IO) { val arguments = mapOf("special" to specialCharsContent) - val result = client.callTool(specialCharsToolName, arguments) as CallToolResultBase + val result = client.callTool(specialCharsToolName, arguments) val content = result.content.firstOrNull() as? TextContent assertNotNull(content, "Tool result content should be TextContent") - val text = content.text ?: "" + val text = content.text assertTrue(text.contains(specialCharsContent), "Result should contain the special characters") @@ -714,7 +728,7 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { @Test fun testConcurrentToolCalls() = runTest { val concurrentCount = 10 - val results = mutableListOf() + val results = mutableListOf() runBlocking { repeat(concurrentCount) { index -> @@ -770,15 +784,91 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { val nonExistentToolName = "non-existent-tool" val arguments = mapOf("text" to "Test") - val exception = assertThrows { - runBlocking { - client.callTool(nonExistentToolName, arguments) + val result = runBlocking { + client.callTool(nonExistentToolName, arguments) + } + + assertNotNull(result, "Tool call result should not be null") + assertTrue(result.isError ?: false, "isError should be true for non-existent tool") + + val textContent = result.content.firstOrNull { it is TextContent } as? TextContent + assertNotNull(textContent, "Error content should be present in the result") + + val errorText = textContent.text + assertTrue( + errorText.contains("non-existent-tool") && errorText.contains("not found"), + "Error message should indicate the tool was not found", + ) + } + + @Test + fun testListToolsPagination() = runBlocking(Dispatchers.IO) { + val receivedCursors = CopyOnWriteArrayList() + val page1 = listOf( + Tool(name = "t-1", inputSchema = ToolSchema()), + Tool(name = "t-2", inputSchema = ToolSchema()), + ) + val page2 = listOf( + Tool(name = "t-3", inputSchema = ToolSchema()), + Tool(name = "t-4", inputSchema = ToolSchema()), + ) + val page3 = listOf(Tool(name = "t-5", inputSchema = ToolSchema())) + + server.sessions.forEach { (_, session) -> + session.setRequestHandler(Method.Defined.ToolsList) { request, _ -> + receivedCursors += request.cursor + when (request.cursor) { + null -> ListToolsResult(tools = page1, nextCursor = "cursor-2") + "cursor-2" -> ListToolsResult(tools = page2, nextCursor = "cursor-3") + "cursor-3" -> ListToolsResult(tools = page3, nextCursor = null) + else -> error("Unexpected cursor: ${request.cursor}") + } } } - val msg = exception.message ?: "" - val expectedMessage = "JSONRPCError(code=InternalError, message=Tool not found: non-existent-tool, data={})" + val collected = mutableListOf() + var cursor: String? = null + do { + val request = if (cursor == null) { + ListToolsRequest() + } else { + ListToolsRequest(PaginatedRequestParams(cursor = cursor)) + } + val response = client.listTools(request) + collected += response.tools + cursor = response.nextCursor + } while (cursor != null) - assertEquals(expectedMessage, msg, "Unexpected error message for non-existent tool") + assertEquals( + listOf(null, "cursor-2", "cursor-3"), + receivedCursors.toList(), + "Client must forward each nextCursor into the next request", + ) + assertEquals( + listOf("t-1", "t-2", "t-3", "t-4", "t-5"), + collected.map { it.name }, + "Client must accumulate pages in order without duplicates or reordering", + ) + } + + @Test + fun testListToolsInvalidCursor() = runBlocking(Dispatchers.IO) { + val invalidCursor = "not-a-valid-cursor" + + server.sessions.forEach { (_, session) -> + session.setRequestHandler(Method.Defined.ToolsList) { _, _ -> + throw McpException( + code = RPCError.ErrorCode.INVALID_PARAMS, + message = "Invalid cursor: $invalidCursor", + ) + } + } + + val exception = assertFailsWith { + client.listTools(ListToolsRequest(PaginatedRequestParams(cursor = invalidCursor))) + } + + assertEquals(RPCError.ErrorCode.INVALID_PARAMS, exception.code) + assertEquals("Invalid cursor: $invalidCursor", exception.message) } } diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt new file mode 100644 index 0000000..2f8b387 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt @@ -0,0 +1,255 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.kotlin + +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.sse.SSE +import io.ktor.serialization.kotlinx.json.json +import io.ktor.server.application.install +import io.ktor.server.engine.EmbeddedServer +import io.ktor.server.engine.embeddedServer +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation +import io.ktor.server.routing.routing +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.SseClientTransport +import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport +import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport +import io.modelcontextprotocol.kotlin.sdk.server.StreamableHttpServerTransport +import io.modelcontextprotocol.kotlin.sdk.server.mcp +import io.modelcontextprotocol.kotlin.sdk.server.mcpStreamableHttp +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.McpJson +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.test.utils.Retry +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import kotlinx.io.Sink +import kotlinx.io.Source +import kotlinx.io.asSink +import kotlinx.io.asSource +import kotlinx.io.buffered +import org.awaitility.kotlin.await +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import java.io.PipedInputStream +import java.io.PipedOutputStream +import kotlin.time.Duration.Companion.seconds +import io.ktor.server.cio.CIO as ServerCIO +import io.ktor.server.sse.SSE as ServerSSE + +@Retry(times = 3) +abstract class KotlinTestBase { + + protected val host = "localhost" + protected var port: Int = 0 + + protected lateinit var server: Server + protected lateinit var client: Client + protected lateinit var serverEngine: EmbeddedServer<*, *> + + // Transport selection + protected enum class TransportKind { SSE, STDIO, STREAMABLE_HTTP } + + protected open val transportKind: TransportKind = TransportKind.STDIO + + // STDIO-specific fields + private var stdioServerTransport: StdioServerTransport? = null + private var stdioClientInput: Source? = null + private var stdioClientOutput: Sink? = null + + // StreamableHTTP-specific fields + private var streamableHttpServerTransport: StreamableHttpServerTransport? = null + + protected abstract fun configureServerCapabilities(): ServerCapabilities + protected abstract fun configureServer() + + @BeforeEach + fun setUp() { + setupServer() + if (transportKind == TransportKind.SSE || transportKind == TransportKind.STREAMABLE_HTTP) { + await + .ignoreExceptions() + .until { + port = runBlocking { serverEngine.engine.resolvedConnectors().first().port } + port != 0 + } + } + runBlocking { + setupClient() + } + } + + protected suspend fun setupClient() { + when (transportKind) { + TransportKind.SSE -> { + val transport = SseClientTransport( + HttpClient(CIO) { + install(SSE) + }, + "http://$host:$port", + ) + client = Client( + Implementation("test", "1.0"), + ) + client.connect(transport) + } + + TransportKind.STDIO -> { + val input = checkNotNull(stdioClientInput) { "STDIO client input not initialized" } + val output = checkNotNull(stdioClientOutput) { "STDIO client output not initialized" } + val transport = StdioClientTransport( + input = input, + output = output, + ) + client = Client( + Implementation("test", "1.0"), + ) + client.connect(transport) + } + + TransportKind.STREAMABLE_HTTP -> { + val transport = StreamableHttpClientTransport( + client = HttpClient(CIO) { + install(SSE) + }, + url = "http://$host:$port/mcp", + ) + client = Client( + Implementation("test", "1.0"), + ) + client.connect(transport) + } + } + } + + protected fun setupServer() { + val capabilities = configureServerCapabilities() + + server = Server( + Implementation(name = "test-server", version = "1.0"), + ServerOptions(capabilities = capabilities), + ) + + configureServer() + + when (transportKind) { + TransportKind.SSE -> { + serverEngine = embeddedServer(ServerCIO, host = host, port = port) { + install(ServerSSE) + routing { + mcp { server } + } + }.start(wait = false) + } + + TransportKind.STREAMABLE_HTTP -> { + // Create StreamableHTTP server transport + // Using JSON response mode for simpler testing (no SSE session required) + val transport = StreamableHttpServerTransport( + StreamableHttpServerTransport.Configuration( + enableJsonResponse = true, // Use JSON response mode for testing + ), + ) + // Use stateless mode to skip session validation for simpler testing + transport.setSessionIdGenerator(null) + streamableHttpServerTransport = transport + + // IMPORTANT: Create server session BEFORE starting the HTTP server + // This ensures message handlers are set up before any requests come in + runBlocking { + server.createSession(transport) + } + + // Start embedded server with routing for StreamableHTTP + serverEngine = embeddedServer(ServerCIO, host = host, port = port) { + // Install ContentNegotiation for JSON serialization + install(ContentNegotiation) { + json(McpJson) + } + mcpStreamableHttp(path = "/mcp") { server } + }.start(wait = false) + } + + TransportKind.STDIO -> { + // Create in-memory stdio pipes: client->server and server->client + val clientToServerOut = PipedOutputStream() + val clientToServerIn = PipedInputStream(clientToServerOut) + + val serverToClientOut = PipedOutputStream() + val serverToClientIn = PipedInputStream(serverToClientOut) + + // Server transport reads from client and writes to client + val serverTransport = StdioServerTransport( + inputStream = clientToServerIn.asSource().buffered(), + outputStream = serverToClientOut.asSink().buffered(), + ) + stdioServerTransport = serverTransport + + // Prepare client-side streams for later client initialization + stdioClientInput = serverToClientIn.asSource().buffered() + stdioClientOutput = clientToServerOut.asSink().buffered() + + // Start server transport by connecting the server + runBlocking { + server.createSession(serverTransport) + } + } + } + } + + @AfterEach + fun tearDown() { + // close client + if (::client.isInitialized) { + try { + runBlocking { + withTimeout(3.seconds) { + client.close() + } + } + } catch (e: Exception) { + println("Warning: Error during client close: ${e.message}") + } + } + + // stop server + when (transportKind) { + TransportKind.SSE, TransportKind.STREAMABLE_HTTP -> { + if (::serverEngine.isInitialized) { + try { + serverEngine.stop(500, 1000) + } catch (e: Exception) { + println("Warning: Error during server stop: ${e.message}") + } + } + if (transportKind == TransportKind.STREAMABLE_HTTP) { + streamableHttpServerTransport?.let { + try { + runBlocking { it.close() } + } catch (e: Exception) { + println("Warning: Error during streamable http server stop: ${e.message}") + } finally { + streamableHttpServerTransport = null + } + } + } + } + + TransportKind.STDIO -> { + stdioServerTransport?.let { + try { + runBlocking { it.close() } + } catch (e: Exception) { + println("Warning: Error during stdio server stop: ${e.message}") + } finally { + stdioServerTransport = null + stdioClientInput = null + stdioClientOutput = null + } + } + } + } + } +} diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/sse/PromptIntegrationTestSse.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/sse/PromptIntegrationTestSse.kt similarity index 75% rename from kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/sse/PromptIntegrationTestSse.kt rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/sse/PromptIntegrationTestSse.kt index d8f218b..10cde27 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/sse/PromptIntegrationTestSse.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/sse/PromptIntegrationTestSse.kt @@ -2,6 +2,6 @@ package io.modelcontextprotocol.kotlin.sdk.integration.kotlin.sse import io.modelcontextprotocol.kotlin.sdk.integration.kotlin.AbstractPromptIntegrationTest -class PromptIntegrationTestSse : AbstractPromptIntegrationTest() { +class SchemaPromptIntegrationTestSse : AbstractPromptIntegrationTest() { override val transportKind: TransportKind = TransportKind.SSE } diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/sse/ResourceIntegrationTestSse.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/sse/ResourceIntegrationTestSse.kt similarity index 100% rename from kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/sse/ResourceIntegrationTestSse.kt rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/sse/ResourceIntegrationTestSse.kt diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/sse/ToolIntegrationTestSse.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/sse/ToolIntegrationTestSse.kt similarity index 100% rename from kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/sse/ToolIntegrationTestSse.kt rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/sse/ToolIntegrationTestSse.kt diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/stdio/PromptIntegrationTestStdio.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/stdio/PromptIntegrationTestStdio.kt similarity index 100% rename from kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/stdio/PromptIntegrationTestStdio.kt rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/stdio/PromptIntegrationTestStdio.kt diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/stdio/ResourceIntegrationTestStdio.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/stdio/ResourceIntegrationTestStdio.kt similarity index 100% rename from kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/stdio/ResourceIntegrationTestStdio.kt rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/stdio/ResourceIntegrationTestStdio.kt diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/stdio/ToolIntegrationTestStdio.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/stdio/ToolIntegrationTestStdio.kt similarity index 100% rename from kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/stdio/ToolIntegrationTestStdio.kt rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/stdio/ToolIntegrationTestStdio.kt diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/streamablehttp/PromptIntegrationTestStreamableHttp.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/streamablehttp/PromptIntegrationTestStreamableHttp.kt new file mode 100644 index 0000000..8f4e334 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/streamablehttp/PromptIntegrationTestStreamableHttp.kt @@ -0,0 +1,7 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.kotlin.streamablehttp + +import io.modelcontextprotocol.kotlin.sdk.integration.kotlin.AbstractPromptIntegrationTest + +class PromptIntegrationTestStreamableHttp : AbstractPromptIntegrationTest() { + override val transportKind: TransportKind = TransportKind.STREAMABLE_HTTP +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/streamablehttp/ResourceIntegrationTestStreamableHttp.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/streamablehttp/ResourceIntegrationTestStreamableHttp.kt new file mode 100644 index 0000000..d72bcfd --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/streamablehttp/ResourceIntegrationTestStreamableHttp.kt @@ -0,0 +1,7 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.kotlin.streamablehttp + +import io.modelcontextprotocol.kotlin.sdk.integration.kotlin.AbstractResourceIntegrationTest + +class ResourceIntegrationTestStreamableHttp : AbstractResourceIntegrationTest() { + override val transportKind: TransportKind = TransportKind.STREAMABLE_HTTP +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/streamablehttp/ToolIntegrationTestStreamableHttp.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/streamablehttp/ToolIntegrationTestStreamableHttp.kt new file mode 100644 index 0000000..288ff07 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/streamablehttp/ToolIntegrationTestStreamableHttp.kt @@ -0,0 +1,7 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.kotlin.streamablehttp + +import io.modelcontextprotocol.kotlin.sdk.integration.kotlin.AbstractToolIntegrationTest + +class ToolIntegrationTestStreamableHttp : AbstractToolIntegrationTest() { + override val transportKind: TransportKind = TransportKind.STREAMABLE_HTTP +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/websocket/WebSocketIntegrationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/websocket/WebSocketIntegrationTest.kt new file mode 100644 index 0000000..7d7898e --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/websocket/WebSocketIntegrationTest.kt @@ -0,0 +1,191 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.kotlin.websocket + +import io.ktor.client.HttpClient +import io.ktor.server.application.install +import io.ktor.server.cio.CIOApplicationEngine +import io.ktor.server.engine.EmbeddedServer +import io.ktor.server.engine.embeddedServer +import io.ktor.server.routing.routing +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.mcpWebSocketTransport +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import io.modelcontextprotocol.kotlin.sdk.server.mcpWebSocket +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.PromptArgument +import io.modelcontextprotocol.kotlin.sdk.types.PromptMessage +import io.modelcontextprotocol.kotlin.sdk.types.Role +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlin.test.Test +import kotlin.test.assertTrue +import io.ktor.client.engine.cio.CIO as ClientCIO +import io.ktor.client.plugins.websocket.WebSockets as ClientWebSocket +import io.ktor.server.cio.CIO as ServerCIO +import io.ktor.server.websocket.WebSockets as ServerWebSockets + +class WebSocketIntegrationTest { + + @Test + fun `client should be able to connect to websocket server 2`() = runBlocking(Dispatchers.IO) { + var server: EmbeddedServer? = null + var client: Client? = null + + try { + server = initServer() + val port = server.engine.resolvedConnectors().first().port + client = initClient(serverPort = port) + } finally { + client?.close() + server?.stop(1000, 2000) + } + } + + /** + * Test Case #1: One opened connection, a client gets a prompt + * + * 1. Open WebSocket from Client A. + * 2. Send a POST request from Client A to POST /prompts/get. + * 3. Observe that Client A receives a response related to it. + */ + @Test + fun `single websocket connection`() = runBlocking(Dispatchers.IO) { + var server: EmbeddedServer? = null + var client: Client? = null + + try { + server = initServer() + val port = server.engine.resolvedConnectors().first().port + client = initClient("Client A", port) + + val promptA = getPrompt(client, "Client A") + assertTrue { "Client A" in promptA } + } finally { + client?.close() + server?.stop(1000, 2000) + } + } + + /** + * Test Case #1: Two open connections, each client gets a client-specific prompt + * + * 1. Open WebSocket connection #1 from Client A and note the sessionId= value. + * 2. Open WebSocket connection #2 from Client B and note the sessionId= value. + * 3. Send a POST request to POST /message with the corresponding sessionId#1. + * 4. Observe that Client B (connection #2) receives a response related to sessionId#1. + */ + @Test + fun `multiple websocket connections`() = runBlocking(Dispatchers.IO) { + var server: EmbeddedServer? = null + var clientA: Client? = null + var clientB: Client? = null + + try { + server = initServer() + val port = server.engine.resolvedConnectors().first().port + clientA = initClient("Client A", port) + clientB = initClient("Client B", port) + + // Step 3: Send a prompt request from Client A + val promptA = getPrompt(clientA, "Client A") + // Step 4: Send a prompt request from Client B + val promptB = getPrompt(clientB, "Client B") + + assertTrue { "Client A" in promptA } + assertTrue { "Client B" in promptB } + } finally { + clientA?.close() + clientB?.close() + server?.stop(1000, 2000) + } + } + + private suspend fun initClient(name: String = "", serverPort: Int): Client { + val client = Client( + Implementation(name = name, version = "1.0.0"), + ) + + val httpClient = HttpClient(ClientCIO) { + install(ClientWebSocket) + } + + // Create a transport wrapper that captures the session ID and received messages + val transport = httpClient.mcpWebSocketTransport { + url { + host = URL + port = serverPort + } + } + + client.connect(transport) + + return client + } + + private fun initServer(): EmbeddedServer { + val server = Server( + Implementation(name = "websocket-server", version = "1.0.0"), + ServerOptions( + capabilities = ServerCapabilities(prompts = ServerCapabilities.Prompts(listChanged = true)), + ), + ) + + server.addPrompt( + name = "prompt", + description = "Prompt description", + arguments = listOf( + PromptArgument( + name = "client", + description = "Client name who requested a prompt", + required = true, + ), + ), + ) { request -> + GetPromptResult( + messages = listOf( + PromptMessage( + role = Role.User, + content = TextContent("Prompt for client ${request.params.arguments?.get("client")}"), + ), + ), + description = "Prompt for ${request.params.name}", + ) + } + + val ktorServer = embeddedServer(ServerCIO, host = URL, port = PORT) { + install(ServerWebSockets) + routing { + mcpWebSocket(block = { server }) + } + } + + return ktorServer.start(wait = false) + } + + /** + * Retrieves a prompt result using the provided client and client name. + */ + private suspend fun getPrompt(client: Client, clientName: String): String { + val response = client.getPrompt( + GetPromptRequest( + GetPromptRequestParams( + name = "prompt", + arguments = mapOf("client" to clientName), + ), + ), + ) + + return (response.messages.first().content as? TextContent)?.text + ?: error("Failed to receive prompt for Client $clientName") + } + + companion object { + private const val PORT = 0 + private const val URL = "127.0.0.1" + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/sse/AbstractSseIntegrationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/sse/AbstractSseIntegrationTest.kt new file mode 100644 index 0000000..2a2f381 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/sse/AbstractSseIntegrationTest.kt @@ -0,0 +1,103 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.sse + +import io.ktor.client.HttpClient +import io.ktor.client.plugins.sse.SSE +import io.ktor.server.application.install +import io.ktor.server.cio.CIOApplicationEngine +import io.ktor.server.engine.EmbeddedServer +import io.ktor.server.engine.embeddedServer +import io.ktor.server.routing.routing +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.mcpSseTransport +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import io.modelcontextprotocol.kotlin.sdk.server.mcp +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.PromptArgument +import io.modelcontextprotocol.kotlin.sdk.types.PromptMessage +import io.modelcontextprotocol.kotlin.sdk.types.Role +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.ktor.client.engine.cio.CIO as ClientCIO +import io.ktor.server.cio.CIO as ServerCIO +import io.ktor.server.sse.SSE as ServerSSE + +open class AbstractSseIntegrationTest { + + suspend fun initTestClient(serverPort: Int, name: String? = null): Client { + val client = Client( + Implementation(name = name ?: DEFAULT_CLIENT_NAME, version = VERSION), + ) + + val httpClient = HttpClient(ClientCIO) { + install(SSE) + } + + // Create a transport wrapper that captures the session ID and received messages + val transport = httpClient.mcpSseTransport { + url { + host = URL + port = serverPort + } + } + + client.connect(transport) + + return client + } + + suspend fun initTestServer( + name: String? = null, + ): EmbeddedServer { + val server = Server( + Implementation(name = name ?: DEFAULT_SERVER_NAME, version = VERSION), + ServerOptions( + capabilities = ServerCapabilities(prompts = ServerCapabilities.Prompts(listChanged = true)), + ), + ) { + addPrompt( + name = "prompt", + description = "Prompt description", + arguments = listOf( + PromptArgument( + name = "client", + description = "Client name who requested a prompt", + required = true, + ), + ), + ) { request -> + GetPromptResult( + messages = listOf( + PromptMessage( + role = Role.User, + content = TextContent("Prompt for client ${request.params.arguments?.get("client")}"), + ), + ), + description = "Prompt for ${request.params.name}", + ) + } + } + + val ktorServer = embeddedServer( + ServerCIO, + host = URL, + port = PORT, + ) { + install(ServerSSE) + routing { + mcp { server } + } + } + + return ktorServer.startSuspend(wait = false) + } + + companion object { + private const val DEFAULT_CLIENT_NAME = "sse-test-client" + private const val DEFAULT_SERVER_NAME = "sse-test-server" + private const val VERSION = "1.0.0" + private const val URL = "127.0.0.1" + private const val PORT = 0 + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/sse/SseAuthenticationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/sse/SseAuthenticationTest.kt new file mode 100644 index 0000000..da76319 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/sse/SseAuthenticationTest.kt @@ -0,0 +1,41 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.sse + +import io.ktor.client.HttpClient +import io.ktor.client.plugins.sse.SSE +import io.ktor.client.request.basicAuth +import io.ktor.server.application.ApplicationCall +import io.ktor.server.routing.Route +import io.modelcontextprotocol.kotlin.sdk.client.SseClientTransport +import io.modelcontextprotocol.kotlin.sdk.integration.AbstractAuthenticationTest +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.server.mcp +import io.modelcontextprotocol.kotlin.sdk.shared.Transport +import kotlin.test.AfterTest +import io.ktor.client.engine.cio.CIO as ClientCIO + +class SseAuthenticationTest : AbstractAuthenticationTest() { + + private var httpClient: HttpClient? = null + + @AfterTest + fun closeHttpClient() { + httpClient?.close() + httpClient = null + } + + override fun Route.registerMcpServer(serverFactory: ApplicationCall.() -> Server) { + mcp { + serverFactory(call) + } + } + + override fun createClientTransport(baseUrl: String, user: String, pass: String): Transport { + val client = HttpClient(ClientCIO) { install(SSE) } + httpClient = client + return SseClientTransport( + client = client, + urlString = baseUrl, + requestBuilder = { basicAuth(user, pass) }, + ) + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/sse/SseIntegrationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/sse/SseIntegrationTest.kt new file mode 100644 index 0000000..8f0516e --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/sse/SseIntegrationTest.kt @@ -0,0 +1,107 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.sse + +import io.ktor.server.cio.CIOApplicationEngine +import io.ktor.server.engine.EmbeddedServer +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.test.utils.actualPort +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlin.test.Test +import kotlin.test.assertTrue + +class SseIntegrationTest : AbstractSseIntegrationTest() { + + @Test + fun `client should be able to connect to sse server`() = runBlocking(Dispatchers.IO) { + var server: EmbeddedServer? = null + var client: Client? = null + + try { + server = initTestServer() + val port = server.actualPort() + client = initTestClient(serverPort = port) + } finally { + client?.close() + server?.stopSuspend(1000, 2000) + } + } + + /** + * Test Case #1: One opened connection, a client gets a prompt + * + * 1. Open SSE from Client A. + * 2. Send a POST request from Client A to POST /prompts/get. + * 3. Observe that Client A receives a response related to it. + */ + @Test + fun `single sse connection`() = runBlocking(Dispatchers.IO) { + var server: EmbeddedServer? = null + var client: Client? = null + try { + server = initTestServer() + val port = server.actualPort() + client = initTestClient(port, "Client A") + + val promptA = getPrompt(client, "Client A") + assertTrue { "Client A" in promptA } + } finally { + client?.close() + server?.stopSuspend(1000, 2000) + } + } + + /** + * Test Case #1: Two open connections, each client gets a client-specific prompt + * + * 1. Open SSE connection #1 from Client A and note the sessionId= value. + * 2. Open SSE connection #2 from Client B and note the sessionId= value. + * 3. Send a POST request to POST /message with the corresponding sessionId#1. + * 4. Observe that Client B (connection #2) receives a response related to sessionId#1. + */ + @Test + fun `multiple sse connections`() = runBlocking(Dispatchers.IO) { + var server: EmbeddedServer? = null + var clientA: Client? = null + var clientB: Client? = null + + try { + server = initTestServer() + val port = server.actualPort() + + clientA = initTestClient(port, "Client A") + clientB = initTestClient(port, "Client B") + + // Step 3: Send a prompt request from Client A + val promptA = getPrompt(clientA, "Client A") + // Step 4: Send a prompt request from Client B + val promptB = getPrompt(clientB, "Client B") + + assertTrue { "Client A" in promptA } + assertTrue { "Client B" in promptB } + } finally { + clientA?.close() + clientB?.close() + server?.stopSuspend(1000, 2000) + } + } + + /** + * Retrieves a prompt result using the provided client and client name. + */ + private suspend fun getPrompt(client: Client, clientName: String): String { + val response = client.getPrompt( + GetPromptRequest( + GetPromptRequestParams( + name = "prompt", + arguments = mapOf("client" to clientName), + ), + ), + ) + + return (response.messages.first().content as? TextContent)?.text + ?: error("Failed to receive prompt for Client $clientName") + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/AbstractStreamableHttpIntegrationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/AbstractStreamableHttpIntegrationTest.kt new file mode 100644 index 0000000..535393e --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/AbstractStreamableHttpIntegrationTest.kt @@ -0,0 +1,79 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.streamablehttp + +import io.ktor.server.cio.CIOApplicationEngine +import io.ktor.server.engine.EmbeddedServer +import io.ktor.server.engine.embeddedServer +import io.ktor.server.sse.Heartbeat +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import io.modelcontextprotocol.kotlin.sdk.server.mcpStreamableHttp +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.PromptArgument +import io.modelcontextprotocol.kotlin.sdk.types.PromptMessage +import io.modelcontextprotocol.kotlin.sdk.types.Role +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.ktor.server.cio.CIO as ServerCIO + +open class AbstractStreamableHttpIntegrationTest { + + suspend fun initTestServer( + name: String? = null, + sseHeartbeatConfig: (Heartbeat.() -> Unit)? = null, + ): StreamableHttpTestServer { + val mcpServer = Server( + Implementation(name = name ?: DEFAULT_SERVER_NAME, version = VERSION), + ServerOptions( + capabilities = ServerCapabilities(prompts = ServerCapabilities.Prompts(listChanged = true)), + ), + ) { + addPrompt( + name = "prompt", + description = "Prompt description", + arguments = listOf( + PromptArgument( + name = "client", + description = "Client name who requested a prompt", + required = true, + ), + ), + ) { request -> + GetPromptResult( + messages = listOf( + PromptMessage( + role = Role.User, + content = TextContent("Prompt for client ${request.params.arguments?.get("client")}"), + ), + ), + description = "Prompt for ${request.params.name}", + ) + } + } + + val ktorServer = embeddedServer( + ServerCIO, + host = URL, + port = PORT, + ) { + mcpStreamableHttp(sseHeartbeatConfig = sseHeartbeatConfig) { mcpServer } + } + + return StreamableHttpTestServer( + mcpServer = mcpServer, + ktorServer = ktorServer.startSuspend(wait = false), + ) + } + + data class StreamableHttpTestServer( + val mcpServer: Server, + val ktorServer: EmbeddedServer, + ) + + companion object { + internal const val DEFAULT_SERVER_NAME = "streamable-http-test-server" + internal const val VERSION = "1.0.0" + internal const val URL = "127.0.0.1" + internal const val PORT = 0 + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpClientTransportIntegrationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpClientTransportIntegrationTest.kt new file mode 100644 index 0000000..598825f --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpClientTransportIntegrationTest.kt @@ -0,0 +1,270 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.streamablehttp + +import io.kotest.matchers.collections.shouldContain +import io.kotest.matchers.shouldBe +import io.ktor.client.HttpClient +import io.ktor.client.plugins.sse.SSE +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.server.engine.embeddedServer +import io.ktor.server.request.receiveText +import io.ktor.server.response.header +import io.ktor.server.response.respond +import io.ktor.server.response.respondText +import io.ktor.server.response.respondTextWriter +import io.ktor.server.routing.Route +import io.ktor.server.routing.delete +import io.ktor.server.routing.get +import io.ktor.server.routing.post +import io.ktor.server.routing.routing +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions +import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.Tool +import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema +import io.modelcontextprotocol.kotlin.test.utils.actualPort +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonObject +import kotlin.test.Test +import io.ktor.client.engine.cio.CIO as ClientCIO +import io.ktor.server.cio.CIO as ServerCIO + +private const val SESSION_ID_HEADER = "mcp-session-id" +private const val HOST = "127.0.0.1" +private const val MCP_PATH = "/mcp" + +private val INITIALIZE_RESULT = """ + { + "capabilities": {"tools": {"listChanged": false}}, + "protocolVersion": "2025-03-26", + "serverInfo": {"name": "test-server", "version": "1.0.0"} + } +""".trimIndent() + +private val TOOLS_LIST_RESULT = """ + { + "tools": [ + { + "name": "get_weather", + "title": "Weather Information Provider", + "description": "Get current weather information for a location", + "inputSchema": { + "type": "object", + "properties": { + "location": {"type": "string", "description": "City name or zip code"} + }, + "required": ["location"] + }, + "outputSchema": { + "type": "object", + "properties": { + "temperature": {"type": "number", "description": "Temperature, Celsius"} + }, + "required": ["temperature"] + }, + "_meta": {} + } + ] + } +""".trimIndent() + +private const val PROGRESS_NOTIFICATION_1 = + """{"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}""" + +private const val PROGRESS_NOTIFICATION_2 = + """{"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":75,"total":100}}""" + +internal class StreamableHttpClientTransportIntegrationTest { + + @Test + fun `client receives progress notifications via GET SSE and lists tools`(): Unit = runBlocking(Dispatchers.IO) { + val sessionId = "sid-progress" + runWithServer({ + mcpPostRoute(sessionId, extraResults = mapOf("tools/list" to TOOLS_LIST_RESULT)) + get(MCP_PATH) { + call.response.header(SESSION_ID_HEADER, sessionId) + call.respondTextWriter(contentType = ContentType.Text.EventStream) { + write(sseEvent(id = "1", data = PROGRESS_NOTIFICATION_1)) + flush() + delay(200) + write(sseEvent(id = "2", data = PROGRESS_NOTIFICATION_2)) + flush() + awaitCancellation() + } + } + }) { url -> + val client = newClient() + client.connect(StreamableHttpClientTransport(client = newHttpClient(), url = url)) + + val listToolsResult = client.listTools() + + listToolsResult.tools shouldContain Tool( + name = "get_weather", + title = "Weather Information Provider", + description = "Get current weather information for a location", + inputSchema = ToolSchema( + properties = buildJsonObject { + putJsonObject("location") { + put("type", "string") + put("description", "City name or zip code") + } + }, + required = listOf("location"), + ), + outputSchema = ToolSchema( + properties = buildJsonObject { + putJsonObject("temperature") { + put("type", "number") + put("description", "Temperature, Celsius") + } + }, + required = listOf("temperature"), + ), + annotations = null, + meta = EmptyJsonObject, + ) + + client.close() + } + } + + @Test + fun `terminateSession sends DELETE request and clears sessionId`(): Unit = runBlocking(Dispatchers.IO) { + val sessionId = "sid-terminate" + runWithServer({ + mcpPostRoute(sessionId) + get(MCP_PATH) { + call.response.header(SESSION_ID_HEADER, sessionId) + call.respondTextWriter(contentType = ContentType.Text.EventStream) { + awaitCancellation() + } + } + delete(MCP_PATH) { + call.respond(HttpStatusCode.OK) + } + }) { url -> + val client = newClient() + val transport = StreamableHttpClientTransport(client = newHttpClient(), url = url) + client.connect(transport) + + transport.sessionId shouldBe sessionId + transport.terminateSession() + transport.sessionId shouldBe null + + client.close() + } + } + + @Test + fun `client survives 405 on GET SSE`(): Unit = runBlocking(Dispatchers.IO) { + val sessionId = "sid-405" + runWithServer({ + mcpPostRoute(sessionId, extraResults = mapOf("ping" to "{}")) + get(MCP_PATH) { + call.respondText( + text = "", + contentType = ContentType.Text.EventStream, + status = HttpStatusCode.MethodNotAllowed, + ) + } + }) { url -> + val client = newClient() + client.connect(StreamableHttpClientTransport(client = newHttpClient(), url = url)) + + client.ping() + + client.close() + } + } + + @Test + fun `client survives non streaming JSON response on GET SSE`(): Unit = runBlocking(Dispatchers.IO) { + val sessionId = "sid-json" + runWithServer({ + mcpPostRoute(sessionId, extraResults = mapOf("ping" to "{}")) + get(MCP_PATH) { + call.response.header(SESSION_ID_HEADER, sessionId) + call.respondText( + text = "", + contentType = ContentType.Application.Json, + status = HttpStatusCode.OK, + ) + } + }) { url -> + val client = newClient() + client.connect(StreamableHttpClientTransport(client = newHttpClient(), url = url)) + + client.ping() + + client.close() + } + } + + private fun Route.mcpPostRoute(sessionId: String, extraResults: Map = emptyMap()) { + post(MCP_PATH) { + val body = call.receiveText() + val parsed = Json.parseToJsonElement(body).jsonObject + val method = parsed["method"]?.jsonPrimitive?.content + val idJson = parsed["id"]?.toString() ?: "null" + call.response.header(SESSION_ID_HEADER, sessionId) + when { + method == "initialize" -> call.respondText( + text = jsonRpcResult(idJson, INITIALIZE_RESULT), + contentType = ContentType.Application.Json, + status = HttpStatusCode.OK, + ) + + method == "notifications/initialized" -> call.respond(HttpStatusCode.Accepted) + + method != null && method in extraResults -> call.respondText( + text = jsonRpcResult(idJson, extraResults.getValue(method)), + contentType = ContentType.Application.Json, + status = HttpStatusCode.OK, + ) + + else -> call.respond(HttpStatusCode.NotFound) + } + } + } + + private suspend fun runWithServer(routes: Route.() -> Unit, block: suspend (url: String) -> Unit) { + val server = embeddedServer(ServerCIO, host = HOST, port = 0) { + routing { routes() } + }.startSuspend(wait = false) + try { + val port = server.actualPort() + block("http://$HOST:$port$MCP_PATH") + } finally { + server.stopSuspend(1000, 2000) + } + } + + private fun newClient(): Client = Client( + clientInfo = Implementation(name = "client1", version = "1.0.0"), + options = ClientOptions(capabilities = ClientCapabilities()), + ) + + private fun newHttpClient(): HttpClient = HttpClient(ClientCIO) { + install(SSE) + } + + private fun jsonRpcResult(idJson: String, resultJson: String): String = + """{"jsonrpc":"2.0","id":$idJson,"result":$resultJson}""" + + private fun sseEvent(id: String, data: String): String = buildString { + append("event: message\n") + append("id: $id\n") + append("data: $data\n\n") + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpHeartbeatIntegrationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpHeartbeatIntegrationTest.kt new file mode 100644 index 0000000..8019557 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpHeartbeatIntegrationTest.kt @@ -0,0 +1,174 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.streamablehttp + +import io.kotest.matchers.shouldBe +import io.ktor.client.HttpClient +import io.ktor.client.plugins.sse.SSE +import io.ktor.client.request.header +import io.ktor.client.request.post +import io.ktor.client.request.prepareGet +import io.ktor.client.request.setBody +import io.ktor.client.statement.bodyAsChannel +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType +import io.ktor.sse.ServerSentEvent +import io.ktor.utils.io.ByteReadChannel +import io.ktor.utils.io.readUTF8Line +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest +import io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION +import io.modelcontextprotocol.kotlin.sdk.types.toJSON +import io.modelcontextprotocol.kotlin.test.utils.actualPort +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeoutOrNull +import kotlinx.serialization.json.Json +import kotlin.test.Test +import kotlin.test.assertNotNull +import kotlin.time.Duration.Companion.milliseconds +import io.ktor.client.engine.cio.CIO as ClientCIO + +private const val SESSION_ID_HEADER = "mcp-session-id" +private const val PROTOCOL_VERSION_HEADER = "mcp-protocol-version" +private const val DEFAULT_HEARTBEAT_LINE = ": heartbeat" +private const val CUSTOM_HEARTBEAT_LINE = ": mcp-heartbeat" + +class StreamableHttpHeartbeatIntegrationTest : AbstractStreamableHttpIntegrationTest() { + + @Test + fun `GET SSE stream emits configured heartbeat`(): Unit = runBlocking(Dispatchers.IO) { + var server: StreamableHttpTestServer? = null + var httpClient: HttpClient? = null + + try { + server = initTestServer("heartbeat-test") { + period = 50.milliseconds + event = ServerSentEvent(comments = "mcp-heartbeat") + } + val mcpUrl = "http://$URL:${server.ktorServer.actualPort()}/mcp" + httpClient = HttpClient(ClientCIO) { install(SSE) } + val sessionId = initializeSession(httpClient, mcpUrl) + + httpClient.prepareGet(mcpUrl) { + addSseHeaders(sessionId) + }.execute { response -> + response.status shouldBe HttpStatusCode.OK + response.headers[SESSION_ID_HEADER] shouldBe sessionId + + response.bodyAsChannel().readLineMatching(CUSTOM_HEARTBEAT_LINE) shouldBe CUSTOM_HEARTBEAT_LINE + } + } finally { + httpClient?.close() + server?.ktorServer?.stopSuspend(1000, 2000) + } + } + + @Test + fun `GET SSE stream does not emit heartbeat by default`(): Unit = runBlocking(Dispatchers.IO) { + var server: StreamableHttpTestServer? = null + var httpClient: HttpClient? = null + + try { + server = initTestServer("no-heartbeat-test") + val mcpUrl = "http://$URL:${server.ktorServer.actualPort()}/mcp" + httpClient = HttpClient(ClientCIO) { install(SSE) } + val sessionId = initializeSession(httpClient, mcpUrl) + + httpClient.prepareGet(mcpUrl) { + addSseHeaders(sessionId) + }.execute { response -> + response.status shouldBe HttpStatusCode.OK + response.headers[SESSION_ID_HEADER] shouldBe sessionId + + response.bodyAsChannel().readLineMatching(timeoutMillis = 150) { line -> + line.isHeartbeatSseLine() + } shouldBe null + } + } finally { + httpClient?.close() + server?.ktorServer?.stopSuspend(1000, 2000) + } + } + + @Test + fun `GET SSE stream emits configured heartbeat repeatedly`(): Unit = runBlocking(Dispatchers.IO) { + var server: StreamableHttpTestServer? = null + var httpClient: HttpClient? = null + + try { + server = initTestServer("repeating-heartbeat-test") { + period = 50.milliseconds + event = ServerSentEvent(comments = "mcp-heartbeat") + } + val mcpUrl = "http://$URL:${server.ktorServer.actualPort()}/mcp" + httpClient = HttpClient(ClientCIO) { install(SSE) } + val sessionId = initializeSession(httpClient, mcpUrl) + + httpClient.prepareGet(mcpUrl) { + addSseHeaders(sessionId) + }.execute { response -> + response.status shouldBe HttpStatusCode.OK + response.headers[SESSION_ID_HEADER] shouldBe sessionId + + val channel = response.bodyAsChannel() + channel.readLineMatching(CUSTOM_HEARTBEAT_LINE) shouldBe CUSTOM_HEARTBEAT_LINE + channel.readLineMatching(CUSTOM_HEARTBEAT_LINE) shouldBe CUSTOM_HEARTBEAT_LINE + } + } finally { + httpClient?.close() + server?.ktorServer?.stopSuspend(1000, 2000) + } + } + + private suspend fun initializeSession(client: HttpClient, mcpUrl: String): String { + val response = client.post(mcpUrl) { + contentType(ContentType.Application.Json) + header( + HttpHeaders.Accept, + "${ContentType.Application.Json}, ${ContentType.Text.EventStream}", + ) + setBody(Json.encodeToString(buildInitPayload())) + } + + response.status shouldBe HttpStatusCode.OK + return assertNotNull(response.headers[SESSION_ID_HEADER]) + } + + private fun buildInitPayload(): JSONRPCRequest = InitializeRequest( + InitializeRequestParams( + protocolVersion = LATEST_PROTOCOL_VERSION, + capabilities = ClientCapabilities(), + clientInfo = Implementation(name = "heartbeat-test-client", version = "1.0.0"), + ), + ).toJSON() + + private fun io.ktor.client.request.HttpRequestBuilder.addSseHeaders(sessionId: String) { + header(HttpHeaders.Accept, ContentType.Text.EventStream.toString()) + header(SESSION_ID_HEADER, sessionId) + header(PROTOCOL_VERSION_HEADER, LATEST_PROTOCOL_VERSION) + } + + private suspend fun ByteReadChannel.readLineMatching(expectedLine: String, timeoutMillis: Long = 2_000): String? = + readLineMatching(timeoutMillis) { line -> line == expectedLine } + + private suspend fun ByteReadChannel.readLineMatching( + timeoutMillis: Long = 2_000, + matches: (String) -> Boolean, + ): String? = withTimeoutOrNull(timeoutMillis.milliseconds) { + var line = readUTF8Line() + while (line != null) { + if (matches(line)) return@withTimeoutOrNull line + line = readUTF8Line() + } + null + } + + private fun String.isHeartbeatSseLine(): Boolean = this == DEFAULT_HEARTBEAT_LINE || + this == CUSTOM_HEARTBEAT_LINE || + (startsWith(":") && contains("heartbeat", ignoreCase = true)) || + (startsWith("event:") && contains("heartbeat", ignoreCase = true)) +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpIntegrationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpIntegrationTest.kt new file mode 100644 index 0000000..b64df4a --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpIntegrationTest.kt @@ -0,0 +1,65 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.streamablehttp + +import io.ktor.client.HttpClient +import io.ktor.client.plugins.sse.SSE +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.test.utils.actualPort +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlin.test.Test +import kotlin.test.assertTrue +import io.ktor.client.engine.cio.CIO as ClientCIO + +class StreamableHttpIntegrationTest : AbstractStreamableHttpIntegrationTest() { + + @Test + fun `client should receive responses via POST json and server notifications via GET sse`() = + runBlocking(Dispatchers.IO) { + var server: StreamableHttpTestServer? = null + var client: Client? = null + var httpClient: HttpClient? = null + + try { + server = initTestServer() + val port = server.ktorServer.actualPort() + + httpClient = HttpClient(ClientCIO) { install(SSE) } + val transport = StreamableHttpClientTransport( + client = httpClient, + url = "http://$URL:$port/mcp", + ).apply { + protocolVersion = LATEST_PROTOCOL_VERSION + } + + client = Client(Implementation("streamable-http-client", VERSION)) + client.connect(transport) + + val prompt = getPrompt(client, "Client A") + assertTrue { "Client A" in prompt } + } finally { + client?.close() + httpClient?.close() + server?.ktorServer?.stopSuspend(1000, 2000) + } + } + + private suspend fun getPrompt(client: Client, clientName: String): String { + val response = client.getPrompt( + GetPromptRequest( + GetPromptRequestParams( + name = "prompt", + arguments = mapOf("client" to clientName), + ), + ), + ) + + return (response.messages.first().content as? TextContent)?.text + ?: error("Failed to receive prompt for Client $clientName") + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpSseReconnectTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpSseReconnectTest.kt new file mode 100644 index 0000000..751f8dd --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpSseReconnectTest.kt @@ -0,0 +1,171 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.streamablehttp + +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.ktor.client.HttpClient +import io.ktor.client.plugins.sse.SSE +import io.ktor.client.request.header +import io.ktor.client.request.post +import io.ktor.client.request.prepareGet +import io.ktor.client.request.setBody +import io.ktor.client.statement.bodyAsChannel +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType +import io.ktor.utils.io.readUTF8Line +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest +import io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION +import io.modelcontextprotocol.kotlin.sdk.types.toJSON +import io.modelcontextprotocol.kotlin.test.utils.actualPort +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json +import kotlin.test.Test +import kotlin.test.assertNotNull +import io.ktor.client.engine.cio.CIO as ClientCIO + +private const val SESSION_ID_HEADER = "mcp-session-id" +private const val PROTOCOL_VERSION_HEADER = "mcp-protocol-version" + +/** + * Integration tests for GET SSE stream reconnection using a real embedded CIO server. + * + * Verifies that the transport correctly evicts stale STANDALONE_SSE_STREAM_ID + * entries when a client reconnects after a disconnect, rather than silently + * rejecting the new stream. + */ +class StreamableHttpSseReconnectTest : AbstractStreamableHttpIntegrationTest() { + + /** + * Verifies that after a GET SSE stream disconnects and the client + * immediately reconnects, the server evicts the stale stream mapping + * and allows the new stream to succeed. + */ + @Test + fun `GET SSE reconnect after disconnect should succeed`(): Unit = runBlocking(Dispatchers.IO) { + var server: StreamableHttpTestServer? = null + var httpClient: HttpClient? = null + + try { + server = initTestServer("reconnect-test") + val port = server.ktorServer.actualPort() + val mcpUrl = "http://$URL:$port/mcp" + + httpClient = HttpClient(ClientCIO) { install(SSE) } + + // Step 1: Initialize session via POST + val initResponse = httpClient.post(mcpUrl) { + contentType(ContentType.Application.Json) + header( + HttpHeaders.Accept, + "${ContentType.Application.Json}, ${ContentType.Text.EventStream}", + ) + setBody(Json.encodeToString(buildInitPayload())) + } + initResponse.status shouldBe HttpStatusCode.OK + val sessionId = assertNotNull(initResponse.headers[SESSION_ID_HEADER]) + + // Step 2: Open GET SSE stream, consume the flush event, then disconnect + httpClient.prepareGet(mcpUrl) { + header(HttpHeaders.Accept, ContentType.Text.EventStream.toString()) + header(SESSION_ID_HEADER, sessionId) + header(PROTOCOL_VERSION_HEADER, LATEST_PROTOCOL_VERSION) + }.execute { response -> + response.status shouldBe HttpStatusCode.OK + response.bodyAsChannel().readUTF8Line() + } + + // Step 3: Immediately reconnect. The transport detects that the + // previous stream's coroutine is no longer active and evicts the + // stale mapping, allowing the new stream to succeed. + httpClient.prepareGet(mcpUrl) { + header(HttpHeaders.Accept, ContentType.Text.EventStream.toString()) + header(SESSION_ID_HEADER, sessionId) + header(PROTOCOL_VERSION_HEADER, LATEST_PROTOCOL_VERSION) + }.execute { response -> + response.status shouldBe HttpStatusCode.OK + response.headers[SESSION_ID_HEADER] shouldBe sessionId + + val channel = response.bodyAsChannel() + val firstLine = channel.readUTF8Line() + firstLine.shouldNotBeNull() + channel.isClosedForRead shouldBe false + } + } finally { + httpClient?.close() + server?.ktorServer?.stopSuspend(1000, 2000) + } + } + + /** + * Verifies that a second concurrent GET SSE stream on the same session + * closes the old stream and takes over. The new stream should be live. + */ + @Test + fun `concurrent GET SSE stream closes old stream and takes over`(): Unit = runBlocking(Dispatchers.IO) { + var server: StreamableHttpTestServer? = null + var httpClient: HttpClient? = null + + try { + server = initTestServer("takeover-test") + val port = server.ktorServer.actualPort() + val mcpUrl = "http://$URL:$port/mcp" + + httpClient = HttpClient(ClientCIO) { install(SSE) } + + // Step 1: Initialize session via POST + val initResponse = httpClient.post(mcpUrl) { + contentType(ContentType.Application.Json) + header( + HttpHeaders.Accept, + "${ContentType.Application.Json}, ${ContentType.Text.EventStream}", + ) + setBody(Json.encodeToString(buildInitPayload())) + } + initResponse.status shouldBe HttpStatusCode.OK + val sessionId = assertNotNull(initResponse.headers[SESSION_ID_HEADER]) + + // Step 2: Open first GET SSE stream and keep it open + httpClient.prepareGet(mcpUrl) { + header(HttpHeaders.Accept, ContentType.Text.EventStream.toString()) + header(SESSION_ID_HEADER, sessionId) + header(PROTOCOL_VERSION_HEADER, LATEST_PROTOCOL_VERSION) + }.execute { firstResponse -> + firstResponse.status shouldBe HttpStatusCode.OK + firstResponse.bodyAsChannel().readUTF8Line() + + // Step 3: Open a second GET — closes old stream, new one takes over + httpClient.prepareGet(mcpUrl) { + header(HttpHeaders.Accept, ContentType.Text.EventStream.toString()) + header(SESSION_ID_HEADER, sessionId) + header(PROTOCOL_VERSION_HEADER, LATEST_PROTOCOL_VERSION) + }.execute { secondResponse -> + secondResponse.status shouldBe HttpStatusCode.OK + secondResponse.headers[SESSION_ID_HEADER] shouldBe sessionId + + // New stream is alive + val secondChannel = secondResponse.bodyAsChannel() + val firstLine = secondChannel.readUTF8Line() + firstLine.shouldNotBeNull() + secondChannel.isClosedForRead shouldBe false + } + } + } finally { + httpClient?.close() + server?.ktorServer?.stopSuspend(1000, 2000) + } + } + + private fun buildInitPayload(): JSONRPCRequest = InitializeRequest( + InitializeRequestParams( + protocolVersion = LATEST_PROTOCOL_VERSION, + capabilities = ClientCapabilities(), + clientInfo = Implementation(name = "reconnect-test-client", version = "1.0.0"), + ), + ).toJSON() +} diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerEdgeCasesTestStdio.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractKotlinClientTsServerEdgeCasesTest.kt similarity index 53% rename from kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerEdgeCasesTestStdio.kt rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractKotlinClientTsServerEdgeCasesTest.kt index 88a4685..d977bd0 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerEdgeCasesTestStdio.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractKotlinClientTsServerEdgeCasesTest.kt @@ -1,10 +1,7 @@ -package io.modelcontextprotocol.kotlin.sdk.integration.typescript.stdio +package io.modelcontextprotocol.kotlin.sdk.integration.typescript -import io.modelcontextprotocol.kotlin.sdk.CallToolResult -import io.modelcontextprotocol.kotlin.sdk.TextContent import io.modelcontextprotocol.kotlin.sdk.client.Client -import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind -import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TsTestBase +import io.modelcontextprotocol.kotlin.sdk.types.TextContent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -14,52 +11,52 @@ import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import org.junit.jupiter.api.Test import org.junit.jupiter.api.Timeout -import org.junit.jupiter.api.assertThrows import java.util.concurrent.TimeUnit import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue -class KotlinClientTsServerEdgeCasesTestStdio : TsTestBase() { +abstract class AbstractKotlinClientTsServerEdgeCasesTest : TsTestBase() { - override val transportKind = TransportKind.STDIO + protected abstract suspend fun useClient(block: suspend (Client) -> T): T @Test @Timeout(30, unit = TimeUnit.SECONDS) - fun testNonExistentToolOverStdio(): Unit = runBlocking(Dispatchers.IO) { - withClientStdio { client: Client, _ -> + fun testNonExistentTool(): Unit = runBlocking(Dispatchers.IO) { + useClient { client -> val nonExistentToolName = "non-existent-tool" val arguments = mapOf("name" to "TestUser") - val exception = assertThrows { - client.callTool(nonExistentToolName, arguments) - } + val result = client.callTool(nonExistentToolName, arguments) + assertNotNull(result, "Tool call result should not be null") - val expectedMessage = - "JSONRPCError(code=InvalidParams, message=MCP error -32602: Tool non-existent-tool not found, data={})" - assertEquals( - expectedMessage, - exception.message, - "Unexpected error message for non-existent tool", + assertTrue(result.isError ?: false, "isError should be true for non-existent tool") + + val textContent = result.content.firstOrNull { it is TextContent } as? TextContent + assertNotNull(textContent, "Error content should be present in the result") + + val errorText = textContent.text + assertTrue( + errorText.contains("non-existent-tool") && errorText.contains("not found"), + "Error message should indicate the tool was not found", ) } } @Test @Timeout(30, unit = TimeUnit.SECONDS) - fun testSpecialCharactersInArgumentsOverStdio(): Unit = runBlocking(Dispatchers.IO) { - withClientStdio { client: Client, _ -> - val specialChars = "!@#$%^&*()_+{}[]|\\:;\"'<>.,?/" + fun testSpecialCharactersInArguments(): Unit = runBlocking(Dispatchers.IO) { + useClient { client -> + val specialChars = "!@#$%^&*()_+{}[]|\\:;\"'<>,.?/" val arguments = mapOf("name" to specialChars) val result = client.callTool("greet", arguments) assertNotNull(result, "Tool call result should not be null") - val callResult = result as CallToolResult - val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent + val textContent = result.content.firstOrNull { it is TextContent } as? TextContent assertNotNull(textContent, "Text content should be present in the result") - val text = textContent.text ?: "" + val text = textContent.text assertTrue( text.contains(specialChars), "Tool response should contain the special characters", @@ -69,19 +66,18 @@ class KotlinClientTsServerEdgeCasesTestStdio : TsTestBase() { @Test @Timeout(30, unit = TimeUnit.SECONDS) - fun testLargePayloadOverStdio(): Unit = runBlocking(Dispatchers.IO) { - withClientStdio { client: Client, _ -> + fun testLargePayload(): Unit = runBlocking(Dispatchers.IO) { + useClient { client -> val largeName = "A".repeat(10 * 1024) val arguments = mapOf("name" to largeName) val result = client.callTool("greet", arguments) assertNotNull(result, "Tool call result should not be null") - val callResult = result as CallToolResult - val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent + val textContent = result.content.firstOrNull { it is TextContent } as? TextContent assertNotNull(textContent, "Text content should be present in the result") - val text = textContent.text ?: "" + val text = textContent.text assertTrue( text.contains("Hello,") && text.contains("A"), "Tool response should contain the greeting with the large name", @@ -91,8 +87,8 @@ class KotlinClientTsServerEdgeCasesTestStdio : TsTestBase() { @Test @Timeout(60, unit = TimeUnit.SECONDS) - fun testConcurrentRequestsOverStdio(): Unit = runBlocking(Dispatchers.IO) { - withClientStdio { client: Client, _ -> + fun testConcurrentRequests(): Unit = runBlocking(Dispatchers.IO) { + useClient { client -> val concurrentCount = 5 val responses = coroutineScope { val results = (1..concurrentCount).map { i -> @@ -103,11 +99,10 @@ class KotlinClientTsServerEdgeCasesTestStdio : TsTestBase() { val result = client.callTool("greet", arguments) assertNotNull(result, "Tool call result should not be null for client $i") - val callResult = result as CallToolResult - val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent + val textContent = result.content.firstOrNull { it is TextContent } as? TextContent assertNotNull(textContent, "Text content should be present for client $i") - textContent.text ?: "" + textContent.text } } results.awaitAll() @@ -127,39 +122,32 @@ class KotlinClientTsServerEdgeCasesTestStdio : TsTestBase() { @Test @Timeout(30, unit = TimeUnit.SECONDS) - fun testInvalidArgumentsOverStdio(): Unit = runBlocking(Dispatchers.IO) { - withClientStdio { client: Client, _ -> + fun testInvalidArguments(): Unit = runBlocking(Dispatchers.IO) { + useClient { client -> val invalidArguments = mapOf( "name" to JsonObject(mapOf("nested" to JsonPrimitive("value"))), ) - val exception = assertThrows { - client.callTool("greet", invalidArguments) - } + val result = client.callTool("greet", invalidArguments) + assertNotNull(result, "Tool call result should not be null") - val msg = exception.message ?: "" - val expectedMessage = """ - JSONRPCError(code=InvalidParams, message=MCP error -32602: Invalid arguments for tool greet: [ - { - "code": "invalid_type", - "expected": "string", - "received": "object", - "path": [ - "name" - ], - "message": "Expected string, received object" - } - ], data={}) - """.trimIndent() + assertTrue(result.isError ?: false, "isError should be true for invalid arguments") - assertEquals(expectedMessage, msg, "Unexpected error message for invalid arguments") + val textContent = result.content.firstOrNull { it is TextContent } as? TextContent + assertNotNull(textContent, "Error content should be present in the result") + + val errorText = textContent.text + assertTrue( + errorText.contains("Invalid arguments") && errorText.contains("greet"), + "Error message should indicate invalid arguments for tool greet", + ) } } @Test @Timeout(30, unit = TimeUnit.SECONDS) - fun testMultipleToolCallsOverStdio(): Unit = runBlocking(Dispatchers.IO) { - withClientStdio { client: Client, _ -> + fun testMultipleToolCalls(): Unit = runBlocking(Dispatchers.IO) { + useClient { client -> repeat(10) { i -> val name = "SequentialClient$i" val arguments = mapOf("name" to name) @@ -167,8 +155,7 @@ class KotlinClientTsServerEdgeCasesTestStdio : TsTestBase() { val result = client.callTool("greet", arguments) assertNotNull(result, "Tool call result should not be null for call $i") - val callResult = result as CallToolResult - val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent + val textContent = result.content.firstOrNull { it is TextContent } as? TextContent assertNotNull(textContent, "Text content should be present for call $i") assertEquals( diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractKotlinClientTsServerTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractKotlinClientTsServerTest.kt similarity index 90% rename from kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractKotlinClientTsServerTest.kt rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractKotlinClientTsServerTest.kt index 5117998..9d27a1d 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractKotlinClientTsServerTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractKotlinClientTsServerTest.kt @@ -1,8 +1,7 @@ package io.modelcontextprotocol.kotlin.sdk.integration.typescript -import io.modelcontextprotocol.kotlin.sdk.CallToolResult -import io.modelcontextprotocol.kotlin.sdk.TextContent import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.types.TextContent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Test @@ -16,7 +15,7 @@ abstract class AbstractKotlinClientTsServerTest : TsTestBase() { protected abstract suspend fun useClient(block: suspend (Client) -> T): T @Test - @Timeout(30, unit = TimeUnit.SECONDS) + @Timeout(60, unit = TimeUnit.SECONDS) fun connectsAndPings() = runBlocking(Dispatchers.IO) { useClient { client -> assertNotNull(client, "Client should be initialized") @@ -29,7 +28,7 @@ abstract class AbstractKotlinClientTsServerTest : TsTestBase() { } @Test - @Timeout(30, unit = TimeUnit.SECONDS) + @Timeout(60, unit = TimeUnit.SECONDS) fun listsTools() = runBlocking(Dispatchers.IO) { useClient { client -> val result = client.listTools() @@ -43,14 +42,14 @@ abstract class AbstractKotlinClientTsServerTest : TsTestBase() { } @Test - @Timeout(30, unit = TimeUnit.SECONDS) + @Timeout(60, unit = TimeUnit.SECONDS) fun callGreet() = runBlocking(Dispatchers.IO) { useClient { client -> val testName = "TestUser" val arguments = mapOf("name" to testName) val result = client.callTool("greet", arguments) assertNotNull(result, "Tool call result should not be null") - val callResult = result as CallToolResult + val callResult = result val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent assertNotNull(textContent, "Text content should be present in the result") assertEquals("Hello, $testName!", textContent.text) @@ -58,7 +57,7 @@ abstract class AbstractKotlinClientTsServerTest : TsTestBase() { } @Test - @Timeout(30, unit = TimeUnit.SECONDS) + @Timeout(60, unit = TimeUnit.SECONDS) fun multipleClients() = runBlocking(Dispatchers.IO) { useClient { client1 -> useClient { client2 -> diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractTsClientKotlinServerTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractTsClientKotlinServerTest.kt similarity index 85% rename from kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractTsClientKotlinServerTest.kt rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractTsClientKotlinServerTest.kt index e7950e7..7f71b6a 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractTsClientKotlinServerTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractTsClientKotlinServerTest.kt @@ -1,10 +1,13 @@ package io.modelcontextprotocol.kotlin.sdk.integration.typescript +import io.kotest.assertions.nondeterministic.eventually +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test import org.junit.jupiter.api.Timeout import java.util.concurrent.TimeUnit import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds abstract class AbstractTsClientKotlinServerTest : TsTestBase() { @@ -23,8 +26,10 @@ abstract class AbstractTsClientKotlinServerTest : TsTestBase() { try { val testName = "TestUser" val out = runClient("greet", testName) - assertTrue(out.contains("Text content:"), "Output should contain the text content section.\n$out") - assertTrue(out.contains("Hello, $testName!"), "Tool response should contain the greeting.\n$out") + eventually(5.seconds) { + assertTrue(out.contains("Text content:"), "Output should contain the text content section.\n$out") + assertTrue(out.contains("Hello, $testName!"), "Tool response should contain the greeting.\n$out") + } assertTrue( out.contains("Structured content:"), "Output should contain the structured content section.\n$out", @@ -44,7 +49,7 @@ abstract class AbstractTsClientKotlinServerTest : TsTestBase() { @Test @Timeout(60, unit = TimeUnit.SECONDS) - fun notifications() = runTest { + fun notifications() = runBlocking { beforeServer() try { val name = "NotifUser" @@ -64,7 +69,7 @@ abstract class AbstractTsClientKotlinServerTest : TsTestBase() { @Test @Timeout(120, unit = TimeUnit.SECONDS) - fun multipleClientSequence() = runTest { + fun multipleClientSequence() = runBlocking { beforeServer() try { val out1 = runClient("greet", "FirstClient") diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt new file mode 100644 index 0000000..29a6de7 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt @@ -0,0 +1,267 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.typescript + +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.sse.SSE +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport +import io.modelcontextprotocol.kotlin.sdk.client.mcpStreamableHttp +import io.modelcontextprotocol.kotlin.sdk.integration.typescript.http.KotlinServerForTsClient +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.test.utils.Retry +import io.modelcontextprotocol.kotlin.test.utils.TypeScriptRunner.installDependencies +import io.modelcontextprotocol.kotlin.test.utils.isWindows +import kotlinx.coroutines.withTimeout +import kotlinx.io.Sink +import kotlinx.io.Source +import kotlinx.io.asSink +import kotlinx.io.asSource +import kotlinx.io.buffered +import java.io.BufferedReader +import java.io.File +import java.io.InputStreamReader +import java.util.concurrent.TimeUnit +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +enum class TransportKind { HTTP, STDIO } + +@Retry(times = 3) +abstract class TsTestBase { + + protected abstract val transportKind: TransportKind + + protected val tsClient: TypeScriptClient get() = TypeScriptClient(typescriptDir) + + companion object { + @JvmStatic + protected val projectRoot = File(System.getProperty("user.dir")) + + @JvmStatic + protected val typescriptDir = File(projectRoot, "src/jvmTest/typescript") + + @JvmStatic + private var sharedHttpServer: Process? = null + + @JvmStatic + private var sharedHttpPort: Int = 0 + + init { + installDependencies(typescriptDir) + } + + @JvmStatic + @Synchronized + protected fun getSharedHttpUrl(): String { + if (sharedHttpServer == null || !sharedHttpServer!!.isAlive) { + sharedHttpPort = io.modelcontextprotocol.kotlin.test.utils.findFreePort() + val server = TypeScriptServer(typescriptDir) + sharedHttpServer = server.startHttp(sharedHttpPort) + println("Shared TypeScript HTTP server started on port $sharedHttpPort") + + Runtime.getRuntime().addShutdownHook( + Thread { + sharedHttpServer?.let { + println("Stopping shared TypeScript HTTP server") + io.modelcontextprotocol.kotlin.test.utils.stopProcess(it) + } + }, + ) + } + return "http://localhost:$sharedHttpPort/mcp" + } + + @JvmStatic + protected fun executeCommand( + command: String, + workingDir: File, + allowFailure: Boolean = false, + timeoutSeconds: Long? = null, + ): String { + if (!workingDir.exists()) { + check(workingDir.mkdirs()) { + "Failed to create working directory: ${workingDir.absolutePath}" + } + } + + check(workingDir.isDirectory && workingDir.canRead()) { + "Working directory is not accessible: ${workingDir.absolutePath}" + } + + val processBuilder = if (isWindows) { + ProcessBuilder() + .command("cmd.exe", "/c", command) + } else { + ProcessBuilder() + .command("bash", "-c", command) + } + + val process = processBuilder + .directory(workingDir) + .redirectErrorStream(true) + .start() + + val output = StringBuilder() + BufferedReader(InputStreamReader(process.inputStream)).use { reader -> + var line: String? + while (reader.readLine().also { line = it } != null) { + println(line) + output.append(line).append("\n") + } + } + + if (timeoutSeconds == null) { + val exitCode = process.waitFor() + require(allowFailure || exitCode == 0) { + "Command execution failed with exit code $exitCode: $command\n" + + "Working dir: ${workingDir.absolutePath}\nOutput:\n$output" + } + } else { + process.waitFor(timeoutSeconds, TimeUnit.SECONDS) + } + + return output.toString() + } + } + + protected fun waitForPort(host: String = "localhost", port: Int, timeoutSeconds: Long = 10): Boolean = + io.modelcontextprotocol.kotlin.test.utils.waitForPort(host, port, timeoutSeconds) + + protected fun executeCommandAllowingFailure(command: String, workingDir: File, timeoutSeconds: Long = 20): String = + executeCommand(command, workingDir, allowFailure = true, timeoutSeconds = timeoutSeconds) + + protected fun stopProcess( + process: Process, + waitDuration: Duration = 3.seconds, + name: String = "TypeScript server", + ) { + io.modelcontextprotocol.kotlin.test.utils.stopProcess(process, waitDuration) + println("$name stopped") + } + + // ===== HTTP client helpers ===== + protected suspend fun newClient(serverUrl: String): Client = + HttpClient(CIO) { install(SSE) }.mcpStreamableHttp(serverUrl) + + protected suspend fun withClient(serverUrl: String, block: suspend (Client) -> T): T { + val client = newClient(serverUrl) + return try { + withTimeout(20.seconds) { block(client) } + } finally { + try { + withTimeout(3.seconds) { client.close() } + } catch (_: Exception) { + // ignore errors + } + } + } + + // ===== STDIO client + server helpers ===== + protected fun startTypeScriptServerStdio(): Process { + val server = TypeScriptServer(typescriptDir) + return server.startStdio() + } + + protected suspend fun newClientStdio(process: Process): Client { + val input: Source = process.inputStream.asSource().buffered() + val output: Sink = process.outputStream.asSink().buffered() + val transport = StdioClientTransport(input = input, output = output) + val client = Client(Implementation("test", "1.0")) + client.connect(transport) + return client + } + + protected suspend fun withClientStdio(block: suspend (Client, Process) -> T): T { + val proc = startTypeScriptServerStdio() + val client = newClientStdio(proc) + return try { + withTimeout(20.seconds) { block(client, proc) } + } finally { + try { + withTimeout(3.seconds) { client.close() } + } catch (_: Exception) { + } + try { + stopProcess(proc, name = "TypeScript stdio server") + } catch (_: Exception) { + } + } + } + + // ===== Helpers to run TypeScript client over STDIO against Kotlin server over STDIO ===== + protected fun runStdioClient(vararg args: String): String = tsClient.use { client -> + // Start Node stdio client (it will speak MCP over its stdout/stdin) + val process = client.startStdio(args.toList(), log = false) + + // Create Kotlin server and attach stdio transport to the process streams + val server: Server = KotlinServerForTsClient().createMcpServer() + val transport = StdioServerTransport( + inputStream = process.inputStream.asSource().buffered(), + outputStream = process.outputStream.asSink().buffered(), + ) + + // Connect server in a background thread to avoid blocking + val serverThread = Thread { + try { + kotlinx.coroutines.runBlocking { server.createSession(transport) } + } catch (e: Exception) { + println("[STDIO-SERVER] Error connecting: ${e.message}") + } + } + serverThread.isDaemon = true + serverThread.start() + + // Read ONLY stderr from client for human-readable output. + // Also use it as a signal that the client is done (some Node processes keep the event-loop alive). + val output = StringBuilder() + val doneLatch = java.util.concurrent.CountDownLatch(1) + val errReader = Thread { + try { + process.errorStream.bufferedReader().useLines { lines -> + lines.forEach { line -> + println("[TS-CLIENT-STDIO][err] $line") + output.append(line).append('\n') + if (line.contains("Disconnected from server")) { + doneLatch.countDown() + } + } + } + } catch (e: Exception) { + println("Warning: Error reading stdio client stderr: ${e.message}") + } + } + errReader.isDaemon = true + errReader.start() + + // Wait for a "done" signal first, then give the process a short grace period to exit. + // This avoids hanging under the test's outer timeout (often ~30s). + val doneTimeoutSeconds = System.getenv("MCP_TS_CLIENT_DONE_TIMEOUT_SECONDS")?.toLongOrNull() ?: 25L + val sawDone = doneLatch.await(doneTimeoutSeconds, TimeUnit.SECONDS) + if (sawDone) { + // Encourage Node to exit if it is waiting on stdin. + try { + process.outputStream.close() + } catch (_: Exception) { + } + } + + // Primary completion condition: the process exits. + // (Some error-paths may not print the disconnect line, but should still exit.) + val clientTimeoutSeconds = System.getenv("MCP_TS_CLIENT_TIMEOUT_SECONDS")?.toLongOrNull() ?: 30L + val finished = process.waitFor(clientTimeoutSeconds, TimeUnit.SECONDS) + if (!finished) { + println("Stdio client did not finish in time (${clientTimeoutSeconds}s); destroying") + process.destroyForcibly() + process.waitFor(2, TimeUnit.SECONDS) + } + + try { + kotlinx.coroutines.runBlocking { transport.close() } + } catch (_: Exception) { + } + + output.toString() + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClient.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClient.kt new file mode 100644 index 0000000..109eab4 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClient.kt @@ -0,0 +1,69 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.typescript + +import io.modelcontextprotocol.kotlin.test.utils.TypeScriptRunner +import io.modelcontextprotocol.kotlin.test.utils.stopProcess +import java.io.File + +/** + * Component for starting and managing TypeScript MCP clients for integration tests. + * + * This class provides methods to start TypeScript clients using HTTP or STDIO transports. + * The clients are started using npx tsx to execute TypeScript files directly. + */ +class TypeScriptClient(private val typescriptDir: File) : AutoCloseable { + private var process: Process? = null + + /** + * Starts a TypeScript MCP client using Streamable HTTP transport. + * + * @param arguments Arguments to pass to the client (typically server URL, tool name, and tool args) + * @param log Whether to automatically log process output/error + * @return The started Process + */ + fun startHttp(arguments: List, log: Boolean = true): Process { + val scriptPath = File(typescriptDir, "client/http-client.ts").absolutePath + val proc = TypeScriptRunner.run( + typescriptDir = typescriptDir, + scriptPath = scriptPath, + arguments = arguments, + env = emptyMap(), + redirectErrorStream = false, + logPrefix = "TS-CLIENT-HTTP", + log = log, + ) + process = proc + return proc + } + + /** + * Starts a TypeScript MCP client using STDIO transport. + * + * @param arguments Arguments to pass to the client (typically tool name and tool args) + * @param log Whether to automatically log process output/error + * @return The started Process + */ + fun startStdio(arguments: List, log: Boolean = true): Process { + val scriptPath = File(typescriptDir, "client/stdio-client.ts").absolutePath + val proc = TypeScriptRunner.run( + typescriptDir = typescriptDir, + scriptPath = scriptPath, + arguments = arguments, + env = emptyMap(), + redirectErrorStream = false, + logPrefix = "TS-CLIENT-STDIO", + log = log, + ) + process = proc + return proc + } + + /** + * Stops the currently running client process. + */ + override fun close() { + process?.let { + stopProcess(it) + } + process = null + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptServer.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptServer.kt new file mode 100644 index 0000000..411a212 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptServer.kt @@ -0,0 +1,121 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.typescript + +import io.modelcontextprotocol.kotlin.test.utils.TypeScriptRunner +import io.modelcontextprotocol.kotlin.test.utils.killProcessOnPort +import io.modelcontextprotocol.kotlin.test.utils.stopProcess +import io.modelcontextprotocol.kotlin.test.utils.waitForPort +import org.awaitility.kotlin.await +import java.io.File +import java.util.concurrent.TimeUnit + +/** + * Factory class for starting and managing TypeScript MCP servers for integration tests. + * + * This class provides methods to: + * - Start TypeScript servers using HTTP or STDIO transports via npx tsx + * - Install npm dependencies in the TypeScript test directory + * - Manage server lifecycle (start/stop) + * - Handle port management and process cleanup + * + * The servers are started using npx tsx to execute TypeScript files directly: + * - `npx tsx server/stdio-server.ts` for STDIO transport + * - `npx tsx server/http-server.ts` for HTTP transport + * + * Usage: + * ```kotlin + * val typescriptDir = File("kotlin-sdk-test/src/jvmTest/typescript") + * val server = TypeScriptServer(typescriptDir) + * + * // Start HTTP server + * val port = TypeScriptServer.findFreePort() + * val process = server.startHttp(port) + * + * // Stop server + * server.stop() + * ``` + */ +class TypeScriptServer(private val typescriptDir: File) { + private var process: Process? = null + + /** + * Starts a TypeScript MCP server using Streamable HTTP transport. + * + * @param port The port number to bind the HTTP server to + * @return The started Process + * @throws IllegalStateException if the server fails to start within timeout + */ + fun startHttp(port: Int): Process { + killProcessOnPort(port) + val serverPath = File(typescriptDir, "server/http-server.ts").absolutePath + val proc = TypeScriptRunner.run( + typescriptDir = typescriptDir, + scriptPath = serverPath, + env = mapOf("MCP_PORT" to port.toString()), + redirectErrorStream = true, + logPrefix = "TS-SERVER-HTTP", + ) + process = proc + + check(waitForPort(port = port, timeoutSeconds = 60)) { + proc.destroyForcibly() + "TypeScript HTTP server did not become ready on localhost:$port within timeout" + } + return proc + } + + /** + * Starts a TypeScript MCP server using STDIO transport. + * + * @return The started Process + */ + fun startStdio(): Process { + val serverPath = File(typescriptDir, "server/stdio-server.ts").absolutePath + val proc = TypeScriptRunner.run( + typescriptDir = typescriptDir, + scriptPath = serverPath, + redirectErrorStream = false, + logPrefix = "TS-SERVER-STDIO", + ) + process = proc + + await.atMost(5, TimeUnit.SECONDS) + .pollDelay(200, TimeUnit.MILLISECONDS) + .pollInterval(100, TimeUnit.MILLISECONDS) + .until { proc.isAlive } + + return proc + } + + /** + * Stops the currently running server process. + * + * Attempts graceful shutdown first, then forces termination if necessary. + */ + fun stop() { + process?.let { + stopProcess(it) + } + process = null + } + + companion object { + /** + * Installs npm dependencies in the specified TypeScript directory. + * + * @param typescriptDir The directory containing package.json + * @throws RuntimeException if npm install fails + */ + @JvmStatic + fun installDependencies(typescriptDir: File) { + TypeScriptRunner.installDependencies(typescriptDir) + } + + /** + * Finds an available port by opening and closing a ServerSocket. + * + * @return An available port number + */ + @JvmStatic + fun findFreePort(): Int = io.modelcontextprotocol.kotlin.test.utils.findFreePort() + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinClientTsServerEdgeCasesTestHttp.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinClientTsServerEdgeCasesTestHttp.kt new file mode 100644 index 0000000..016aa1c --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinClientTsServerEdgeCasesTestHttp.kt @@ -0,0 +1,14 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.typescript.http + +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.integration.typescript.AbstractKotlinClientTsServerEdgeCasesTest +import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind + +class KotlinClientTsServerEdgeCasesTestHttp : AbstractKotlinClientTsServerEdgeCasesTest() { + + override val transportKind = TransportKind.HTTP + + private val serverUrl: String by lazy { getSharedHttpUrl() } + + override suspend fun useClient(block: suspend (Client) -> T): T = withClient(serverUrl, block) +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinClientTsServerTestHttp.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinClientTsServerTestHttp.kt new file mode 100644 index 0000000..7d919af --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinClientTsServerTestHttp.kt @@ -0,0 +1,23 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.typescript.http + +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.integration.typescript.AbstractKotlinClientTsServerTest +import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind +import kotlinx.coroutines.withTimeout +import kotlin.time.Duration.Companion.seconds + +class KotlinClientTsServerTestHttp : AbstractKotlinClientTsServerTest() { + override val transportKind = TransportKind.HTTP + + private val serverUrl: String by lazy { getSharedHttpUrl() } + + override suspend fun useClient(block: suspend (Client) -> T): T = withClient(serverUrl) { client -> + try { + withTimeout(20.seconds) { block(client) } + } finally { + try { + withTimeout(3.seconds) { client.close() } + } catch (_: Exception) {} + } + } +} diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinServerForTsClientSse.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinServerForTsClientHttp.kt similarity index 76% rename from kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinServerForTsClientSse.kt rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinServerForTsClientHttp.kt index 56abf31..ebd0499 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinServerForTsClientSse.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinServerForTsClientHttp.kt @@ -1,4 +1,4 @@ -package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse +package io.modelcontextprotocol.kotlin.sdk.integration.typescript.http import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.http.ContentType @@ -17,34 +17,34 @@ import io.ktor.server.routing.delete import io.ktor.server.routing.get import io.ktor.server.routing.post import io.ktor.server.routing.routing -import io.modelcontextprotocol.kotlin.sdk.CallToolResult -import io.modelcontextprotocol.kotlin.sdk.ErrorCode -import io.modelcontextprotocol.kotlin.sdk.GetPromptResult -import io.modelcontextprotocol.kotlin.sdk.Implementation -import io.modelcontextprotocol.kotlin.sdk.JSONRPCError -import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage -import io.modelcontextprotocol.kotlin.sdk.JSONRPCNotification -import io.modelcontextprotocol.kotlin.sdk.JSONRPCRequest -import io.modelcontextprotocol.kotlin.sdk.JSONRPCResponse -import io.modelcontextprotocol.kotlin.sdk.LoggingLevel -import io.modelcontextprotocol.kotlin.sdk.LoggingMessageNotification -import io.modelcontextprotocol.kotlin.sdk.PromptArgument -import io.modelcontextprotocol.kotlin.sdk.PromptMessage -import io.modelcontextprotocol.kotlin.sdk.ReadResourceResult -import io.modelcontextprotocol.kotlin.sdk.RequestId -import io.modelcontextprotocol.kotlin.sdk.Role -import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities -import io.modelcontextprotocol.kotlin.sdk.TextContent -import io.modelcontextprotocol.kotlin.sdk.TextResourceContents -import io.modelcontextprotocol.kotlin.sdk.Tool import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport -import io.modelcontextprotocol.kotlin.sdk.shared.McpJson +import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions +import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCError +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse +import io.modelcontextprotocol.kotlin.sdk.types.McpJson +import io.modelcontextprotocol.kotlin.sdk.types.PromptArgument +import io.modelcontextprotocol.kotlin.sdk.types.PromptMessage +import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult +import io.modelcontextprotocol.kotlin.sdk.types.RequestId +import io.modelcontextprotocol.kotlin.sdk.types.Role +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents +import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement @@ -55,7 +55,6 @@ import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.decodeFromJsonElement import kotlinx.serialization.json.jsonPrimitive -import org.awaitility.Awaitility.await import java.util.UUID import java.util.concurrent.ConcurrentHashMap @@ -134,14 +133,16 @@ class KotlinServerForTsClient { call.response.header("mcp-session-id", newSessionId) - val serverThread = Thread { - runBlocking { - mcpServer.connect(transport) + launch(Dispatchers.IO) { + try { + mcpServer.createSession(transport) + } catch (e: Exception) { + logger.error(e) { "Failed to create MCP session" } + transport.ready.completeExceptionally(e) } } - serverThread.start() - Thread.sleep(500) + transport.ready.await() transport.handleRequest(call, jsonElement) } else { @@ -175,9 +176,7 @@ class KotlinServerForTsClient { logger.info { "Terminating session: $sessionId" } val transport = serverTransports[sessionId]!! serverTransports.remove(sessionId) - runBlocking { - transport.close() - } + transport.close() call.respond(HttpStatusCode.OK) } else { logger.warn { "Invalid session termination request: $sessionId" } @@ -214,7 +213,7 @@ class KotlinServerForTsClient { server.addTool( name = "greet", description = "A simple greeting tool", - inputSchema = Tool.Input( + inputSchema = ToolSchema( properties = buildJsonObject { put( "name", @@ -227,7 +226,7 @@ class KotlinServerForTsClient { required = listOf("name"), ), ) { request -> - val name = (request.arguments["name"] as? JsonPrimitive)?.content ?: "World" + val name = (request.params.arguments?.get("name") as? JsonPrimitive)?.content ?: "World" CallToolResult( content = listOf(TextContent("Hello, $name!")), structuredContent = buildJsonObject { @@ -239,7 +238,7 @@ class KotlinServerForTsClient { server.addTool( name = "multi-greet", description = "A greeting tool that sends multiple notifications", - inputSchema = Tool.Input( + inputSchema = ToolSchema( properties = buildJsonObject { put( "name", @@ -252,33 +251,7 @@ class KotlinServerForTsClient { required = listOf("name"), ), ) { request -> - val name = (request.arguments["name"] as? JsonPrimitive)?.content ?: "World" - - server.sendToolListChanged() - server.sendLoggingMessage( - LoggingMessageNotification( - LoggingMessageNotification.Params( - level = LoggingLevel.info, - data = JsonPrimitive("Preparing greeting for $name"), - ), - ), - ) - server.sendLoggingMessage( - LoggingMessageNotification( - LoggingMessageNotification.Params( - level = LoggingLevel.info, - data = JsonPrimitive("Halfway there for $name"), - ), - ), - ) - server.sendLoggingMessage( - LoggingMessageNotification( - LoggingMessageNotification.Params( - level = LoggingLevel.info, - data = JsonPrimitive("Done sending greetings to $name"), - ), - ), - ) + val name = (request.params.arguments?.get("name") as? JsonPrimitive)?.content ?: "World" CallToolResult( content = listOf(TextContent("Multiple greetings sent to $name!")), @@ -301,15 +274,15 @@ class KotlinServerForTsClient { ), ) { request -> GetPromptResult( - "Greeting for ${request.name}", messages = listOf( PromptMessage( - role = Role.user, + role = Role.User, content = TextContent( - "Please greet ${request.arguments?.get("name") ?: "someone"} in a friendly manner.", + "Please greet ${request.params.arguments?.get("name") ?: "someone"} in a friendly manner.", ), ), ), + description = "Greeting for ${request.params.name}", ) } @@ -321,7 +294,7 @@ class KotlinServerForTsClient { ) { request -> ReadResourceResult( contents = listOf( - TextResourceContents("Hello, world!", request.uri, "text/plain"), + TextResourceContents("Hello, world!", request.params.uri, "text/plain"), ), ) } @@ -341,9 +314,10 @@ class HttpServerTransport(private val sessionId: String) : AbstractTransport() { private val logger = KotlinLogging.logger {} private val pendingResponses = ConcurrentHashMap>() private val messageQueue = Channel(Channel.UNLIMITED) + internal val ready = CompletableDeferred() suspend fun stream(call: ApplicationCall) { - logger.debug { "Starting SSE stream for session: $sessionId" } + logger.debug { "Starting Event stream for session: $sessionId" } call.response.header("Cache-Control", "no-cache") call.response.header("Connection", "keep-alive") call.respondTextWriter(ContentType.Text.EventStream) { @@ -359,29 +333,28 @@ class HttpServerTransport(private val sessionId: String) : AbstractTransport() { flush() } } catch (e: Exception) { - logger.warn(e) { "SSE stream terminated for session: $sessionId" } + logger.warn(e) { "Event stream terminated for session: $sessionId" } } finally { - logger.debug { "SSE stream closed for session: $sessionId" } + logger.debug { "Event stream closed for session: $sessionId" } } } } suspend fun handleRequest(call: ApplicationCall, requestBody: JsonElement) { try { - logger.info { "Handling request body: $requestBody" } + logger.trace { "Handling request body: $requestBody" } val message = McpJson.decodeFromJsonElement(requestBody) - logger.info { "Decoded message: $message" } + logger.debug { "Decoded message: $message" } if (message is JSONRPCRequest) { val id = message.id.toString() - logger.info { "Received request with ID: $id, method: ${message.method}" } + logger.debug { "Received request with ID: $id, method: ${message.method}" } val responseDeferred = CompletableDeferred() pendingResponses[id] = responseDeferred - logger.info { "Created deferred response for ID: $id" } - logger.info { "Invoking onMessage handler" } + logger.trace { "Invoking onMessage handler for ID: $id" } _onMessage.invoke(message) - logger.info { "onMessage handler completed" } + logger.trace { "onMessage handler completed for ID: $id" } try { val response = withTimeoutOrNull(10000) { @@ -395,10 +368,10 @@ class HttpServerTransport(private val sessionId: String) : AbstractTransport() { logger.warn { "Timeout waiting for response to request ID: $id" } call.respondText( McpJson.encodeToString( - JSONRPCResponse( + JSONRPCError( id = message.id, - error = JSONRPCError( - code = ErrorCode.Defined.RequestTimeout, + error = RPCError( + code = RPCError.ErrorCode.REQUEST_TIMEOUT, message = "Request timed out", ), ), @@ -412,10 +385,10 @@ class HttpServerTransport(private val sessionId: String) : AbstractTransport() { if (!call.response.isCommitted) { call.respondText( McpJson.encodeToString( - JSONRPCResponse( + JSONRPCError( id = message.id, - error = JSONRPCError( - code = ErrorCode.Defined.ConnectionClosed, + error = RPCError( + code = RPCError.ErrorCode.CONNECTION_CLOSED, message = "Request cancelled", ), ), @@ -432,10 +405,10 @@ class HttpServerTransport(private val sessionId: String) : AbstractTransport() { logger.error(e) { "Error handling request" } if (!call.response.isCommitted) { try { - val errorResponse = JSONRPCResponse( - id = RequestId.NumberId(0), - error = JSONRPCError( - code = ErrorCode.Defined.InternalError, + val errorResponse = JSONRPCError( + id = RequestId(0), + error = RPCError( + code = RPCError.ErrorCode.INTERNAL_ERROR, message = "Internal server error: ${e.message}", ), ) @@ -454,36 +427,35 @@ class HttpServerTransport(private val sessionId: String) : AbstractTransport() { override suspend fun start() { logger.debug { "Starting HTTP server transport for session: $sessionId" } + ready.complete(Unit) } - override suspend fun send(message: JSONRPCMessage) { - logger.info { "Sending message: $message" } + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { + logger.trace { "Sending message: $message" } if (message is JSONRPCResponse) { val id = message.id.toString() - logger.info { "Sending response for request ID: $id" } + logger.debug { "Completing response for request ID: $id" } val deferred = pendingResponses.remove(id) if (deferred != null) { - logger.info { "Found pending response for ID: $id, completing deferred" } deferred.complete(message) return } else { logger.warn { "No pending response found for ID: $id" } } } else if (message is JSONRPCRequest) { - logger.info { "Sending request with ID: ${message.id}" } + logger.debug { "Sending request with ID: ${message.id}" } } else if (message is JSONRPCNotification) { - logger.info { "Sending notification: ${message.method}" } + logger.debug { "Sending notification: ${message.method}" } } - logger.info { "Queueing message for next client request" } messageQueue.send(message) } override suspend fun close() { logger.debug { "Closing HTTP server transport for session: $sessionId" } messageQueue.close() - _onClose.invoke() + invokeOnCloseCallback() } } diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsClientKotlinServerTestSse.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/TsClientKotlinServerTestHttp.kt similarity index 59% rename from kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsClientKotlinServerTestSse.kt rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/TsClientKotlinServerTestHttp.kt index edee427..eff91e5 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsClientKotlinServerTestSse.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/TsClientKotlinServerTestHttp.kt @@ -1,13 +1,15 @@ -package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse +package io.modelcontextprotocol.kotlin.sdk.integration.typescript.http import io.modelcontextprotocol.kotlin.sdk.integration.typescript.AbstractTsClientKotlinServerTest import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind +import io.modelcontextprotocol.kotlin.test.utils.findFreePort +import io.modelcontextprotocol.kotlin.test.utils.killProcessOnPort import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach -class TsClientKotlinServerTestSse : AbstractTsClientKotlinServerTest() { +class TsClientKotlinServerTestHttp : AbstractTsClientKotlinServerTest() { - override val transportKind = TransportKind.SSE + override val transportKind = TransportKind.HTTP private var port: Int = 0 private lateinit var serverUrl: String @@ -33,18 +35,16 @@ class TsClientKotlinServerTestSse : AbstractTsClientKotlinServerTest() { } } - override fun beforeServer() {} - override fun afterServer() {} - - override fun runClient(vararg args: String): String { - val cmd = buildString { - append("npx tsx myClient.ts ") - append(serverUrl) - if (args.isNotEmpty()) { - append(' ') - append(args.joinToString(" ")) + override fun runClient(vararg args: String): String = tsClient.use { client -> + val process = client.startHttp(listOf(serverUrl) + args.toList(), log = true) + val output = StringBuilder() + process.inputStream.bufferedReader().useLines { lines -> + lines.forEach { line -> + println("[TS-CLIENT-HTTP] $line") + output.append(line).append("\n") } } - return executeCommand(cmd, tsClientDir) + process.waitFor(25, java.util.concurrent.TimeUnit.SECONDS) + output.toString() } } diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsEdgeCasesTestSse.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/TsEdgeCasesTestHttp.kt similarity index 82% rename from kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsEdgeCasesTestSse.kt rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/TsEdgeCasesTestHttp.kt index 65b003d..9326cc6 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsEdgeCasesTestSse.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/TsEdgeCasesTestHttp.kt @@ -1,16 +1,19 @@ -package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse +package io.modelcontextprotocol.kotlin.sdk.integration.typescript.http import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TsTestBase +import io.modelcontextprotocol.kotlin.test.utils.findFreePort +import io.modelcontextprotocol.kotlin.test.utils.killProcessOnPort import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.Timeout -import org.junit.jupiter.api.condition.EnabledOnOs +import org.junit.jupiter.api.condition.DisabledOnOs import org.junit.jupiter.api.condition.OS import java.io.File import java.util.concurrent.TimeUnit @@ -18,9 +21,9 @@ import kotlin.test.assertEquals import kotlin.test.assertTrue import kotlin.test.fail -class TsEdgeCasesTestSse : TsTestBase() { +class TsEdgeCasesTestHttp : TsTestBase() { - override val transportKind = TransportKind.SSE + override val transportKind = TransportKind.HTTP private var port: Int = 0 private lateinit var serverUrl: String @@ -33,8 +36,8 @@ class TsEdgeCasesTestSse : TsTestBase() { killProcessOnPort(port) httpServer = KotlinServerForTsClient() httpServer?.start(port) - if (!waitForPort(port = port)) { - throw IllegalStateException("Kotlin test server did not become ready on localhost:$port within timeout") + check(waitForPort(port = port)) { + "Kotlin test server did not become ready on localhost:$port within timeout" } println("Kotlin server started on port $port") } @@ -52,16 +55,16 @@ class TsEdgeCasesTestSse : TsTestBase() { @Test @Timeout(30, unit = TimeUnit.SECONDS) fun testInvalidURL() = runTest { - val nonExistentToolCommand = "npx tsx myClient.ts $serverUrl non-existent-tool" - val nonExistentToolOutput = executeCommandAllowingFailure(nonExistentToolCommand, tsClientDir) + val nonExistentToolCommand = "npx tsx client/http-client.ts $serverUrl non-existent-tool" + val nonExistentToolOutput = executeCommandAllowingFailure(nonExistentToolCommand, typescriptDir) assertTrue( nonExistentToolOutput.contains("Tool \"non-existent-tool\" not found"), "Client should handle non-existent tool gracefully", ) - val invalidUrlCommand = "npx tsx myClient.ts http://localhost:${port + 1000}/mcp greet TestUser" - val invalidUrlOutput = executeCommandAllowingFailure(invalidUrlCommand, tsClientDir) + val invalidUrlCommand = "npx tsx client/http-client.ts http://localhost:${port + 1000}/mcp greet TestUser" + val invalidUrlOutput = executeCommandAllowingFailure(invalidUrlCommand, typescriptDir) assertTrue( invalidUrlOutput.contains("Invalid URL") || @@ -81,8 +84,8 @@ class TsEdgeCasesTestSse : TsTestBase() { tempFile.deleteOnExit() val specialCharsContent = tempFile.readText() - val specialCharsCommand = "npx tsx myClient.ts $serverUrl greet \"$specialCharsContent\"" - val specialCharsOutput = executeCommand(specialCharsCommand, tsClientDir) + val specialCharsCommand = "npx tsx client/http-client.ts $serverUrl greet \"$specialCharsContent\"" + val specialCharsOutput = executeCommand(specialCharsCommand, typescriptDir) assertTrue( specialCharsOutput.contains("Hello, $specialChars!"), @@ -96,8 +99,8 @@ class TsEdgeCasesTestSse : TsTestBase() { // skip on windows as it can't handle long commands @Test - @Timeout(30, unit = TimeUnit.SECONDS) - @EnabledOnOs(OS.MAC, OS.LINUX) + @Timeout(40, unit = TimeUnit.SECONDS) + @DisabledOnOs(OS.WINDOWS) fun testLargePayload() = runTest { val largeName = "A".repeat(10 * 1024) @@ -106,8 +109,8 @@ class TsEdgeCasesTestSse : TsTestBase() { tempFile.deleteOnExit() val largeNameContent = tempFile.readText() - val largePayloadCommand = "npx tsx myClient.ts $serverUrl greet \"$largeNameContent\"" - val largePayloadOutput = executeCommand(largePayloadCommand, tsClientDir) + val largePayloadCommand = "npx tsx client/http-client.ts $serverUrl greet \"$largeNameContent\"" + val largePayloadOutput = executeCommand(largePayloadCommand, typescriptDir) tempFile.delete() @@ -122,8 +125,8 @@ class TsEdgeCasesTestSse : TsTestBase() { } @Test - @Timeout(60, unit = TimeUnit.SECONDS) - fun testComplexConcurrentRequests() = runTest { + @Timeout(30, unit = TimeUnit.SECONDS) + fun testComplexConcurrentRequests(): Unit = runBlocking { fun prettyFail( index: Int, command: String, @@ -154,18 +157,18 @@ class TsEdgeCasesTestSse : TsTestBase() { } val commands = listOf( - "npx tsx myClient.ts $serverUrl greet \"Client1\"", - "npx tsx myClient.ts $serverUrl multi-greet \"Client2\"", - "npx tsx myClient.ts $serverUrl greet \"Client3\"", - "npx tsx myClient.ts $serverUrl", - "npx tsx myClient.ts $serverUrl multi-greet \"Client5\"", + "npx tsx client/http-client.ts $serverUrl greet \"Client1\"", + "npx tsx client/http-client.ts $serverUrl multi-greet \"Client2\"", + "npx tsx client/http-client.ts $serverUrl greet \"Client3\"", + "npx tsx client/http-client.ts $serverUrl", + "npx tsx client/http-client.ts $serverUrl multi-greet \"Client5\"", ) coroutineScope { val jobs = commands.mapIndexed { index, command -> async(kotlinx.coroutines.Dispatchers.IO) { println("Starting client $index") - val output = executeCommand(command, tsClientDir) + val output = executeCommand(command, typescriptDir) println("Client $index completed") assertContains( @@ -237,10 +240,10 @@ class TsEdgeCasesTestSse : TsTestBase() { @Test @Timeout(120, unit = TimeUnit.SECONDS) - fun testRapidSequentialRequests() = runTest { + fun testRapidSequentialRequests() = runBlocking { val outputs = (1..10).map { i -> - val command = "npx tsx myClient.ts $serverUrl greet \"RapidClient$i\"" - val output = executeCommand(command, tsClientDir) + val command = "npx tsx client/http-client.ts $serverUrl greet \"RapidClient$i\"" + val output = executeCommand(command, typescriptDir) assertTrue( output.contains("Connected to server"), diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerEdgeCasesTestStdio.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerEdgeCasesTestStdio.kt new file mode 100644 index 0000000..b8c8e93 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerEdgeCasesTestStdio.kt @@ -0,0 +1,12 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.typescript.stdio + +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.integration.typescript.AbstractKotlinClientTsServerEdgeCasesTest +import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind + +class KotlinClientTsServerEdgeCasesTestStdio : AbstractKotlinClientTsServerEdgeCasesTest() { + + override val transportKind = TransportKind.STDIO + + override suspend fun useClient(block: suspend (Client) -> T): T = withClientStdio { c, _ -> block(c) } +} diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerTestStdio.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerTestStdio.kt similarity index 60% rename from kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerTestStdio.kt rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerTestStdio.kt index 8273724..928d591 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerTestStdio.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerTestStdio.kt @@ -8,16 +8,7 @@ class KotlinClientTsServerTestStdio : AbstractKotlinClientTsServerTest() { override val transportKind = TransportKind.STDIO - override suspend fun useClient(block: suspend (Client) -> T): T = withClientStdio { client, proc -> - try { - block(client) - } finally { - try { - client.close() - } catch (_: Exception) {} - try { - stopProcess(proc, name = "TypeScript stdio server") - } catch (_: Exception) {} - } - } + override suspend fun useClient(block: suspend (Client) -> T): T = + // TsTestBase.withClientStdio already handles client.close() + process shutdown. + withClientStdio { client, _ -> block(client) } } diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/TsClientKotlinServerTestStdio.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/TsClientKotlinServerTestStdio.kt similarity index 100% rename from kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/TsClientKotlinServerTestStdio.kt rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/TsClientKotlinServerTestStdio.kt diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/TsEdgeCasesTestStdio.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/TsEdgeCasesTestStdio.kt similarity index 100% rename from kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/TsEdgeCasesTestStdio.kt rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/TsEdgeCasesTestStdio.kt diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/AbstractServerFeaturesTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/AbstractServerFeaturesTest.kt new file mode 100644 index 0000000..1d28218 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/AbstractServerFeaturesTest.kt @@ -0,0 +1,77 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions +import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport +import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.BeforeEach + +abstract class AbstractServerFeaturesTest { + + protected lateinit var server: Server + protected lateinit var client: Client + + protected fun addTool(name: String, block: suspend ClientConnection.() -> Unit) { + server.addTool(name, "Test $name") { + block() + CallToolResult(listOf(TextContent("Success"))) + } + } + + protected fun addPrompt(name: String, block: suspend ClientConnection.() -> Unit) { + server.addPrompt(name, "Test $name") { + block() + GetPromptResult(messages = emptyList()) + } + } + + protected fun addResource(uri: String, block: suspend ClientConnection.() -> Unit) { + server.addResource(uri, uri, "Test resource $uri") { + block() + ReadResourceResult(contents = listOf(TextResourceContents(text = "content", uri = uri))) + } + } + + abstract fun getServerCapabilities(): ServerCapabilities + + protected open fun getClientCapabilities(): ClientCapabilities = ClientCapabilities() + + protected open fun getServerInstructionsProvider(): (() -> String)? = null + + @BeforeEach + fun setUp() { + val serverOptions = ServerOptions( + capabilities = getServerCapabilities(), + ) + + server = Server( + serverInfo = Implementation(name = "test server", version = "1.0"), + options = serverOptions, + instructionsProvider = getServerInstructionsProvider(), + ) + + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + + client = Client( + clientInfo = Implementation(name = "test client", version = "1.0"), + options = ClientOptions( + capabilities = getClientCapabilities(), + ), + ) + + runBlocking { + // Connect client and server + launch { client.connect(clientTransport) } + launch { server.createSession(serverTransport) } + } + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionLoggingTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionLoggingTest.kt new file mode 100644 index 0000000..a1cf797 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionLoggingTest.kt @@ -0,0 +1,182 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.kotest.assertions.nondeterministic.continually +import io.kotest.assertions.nondeterministic.eventually +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel +import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification +import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotificationParams +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import java.util.concurrent.CopyOnWriteArrayList +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +class ClientConnectionLoggingTest : AbstractServerFeaturesTest() { + + override fun getServerCapabilities(): ServerCapabilities = ServerCapabilities( + tools = ServerCapabilities.Tools(listChanged = true), + logging = JsonObject(emptyMap()), + ) + + @Test + fun `notification should send logging message to client`(): Unit = runBlocking { + val notificationReceived = CompletableDeferred() + client.setNotificationHandler(Method.Defined.NotificationsMessage) { + notificationReceived.complete(it) + CompletableDeferred(Unit) + } + + val expectedLevel = LoggingLevel.Info + val expectedData = JsonPrimitive("test-data-sample") + + addTool("test-notification") { + notification( + LoggingMessageNotification( + LoggingMessageNotificationParams( + level = expectedLevel, + data = expectedData, + ), + ), + ) + } + + client.callTool(CallToolRequest(CallToolRequestParams("test-notification"))) + val received = notificationReceived.await() + received.params.level shouldBe expectedLevel + received.params.data shouldBe expectedData + } + + @ParameterizedTest + @EnumSource(LoggingLevel::class) + fun `notification should filter logging messages below level`(minLevel: LoggingLevel): Unit = runBlocking { + val receivedMessages = CopyOnWriteArrayList() + client.setNotificationHandler(Method.Defined.NotificationsMessage) { + receivedMessages.add(it) + CompletableDeferred(Unit) + } + + client.setLoggingLevel(minLevel) + + addTool("test-notification-filtered") { + LoggingLevel.entries.forEach { level -> + notification( + LoggingMessageNotification( + LoggingMessageNotificationParams( + level = level, + data = JsonPrimitive(level.name), + ), + ), + ) + } + } + + client.callTool(CallToolRequest(CallToolRequestParams("test-notification-filtered"))) + + // Handlers dispatch on Dispatchers.Default after the handshake, so await the expected count + // before asserting order. + val expectedLevels = LoggingLevel.entries.filter { it >= minLevel } + eventually(5.seconds) { receivedMessages shouldHaveSize expectedLevels.size } + receivedMessages.map { it.params.level } shouldBe expectedLevels + } + + @ParameterizedTest + @EnumSource(LoggingLevel::class) + fun `sendLoggingMessage should send message at level`(expectedLevel: LoggingLevel): Unit = runBlocking { + val notificationReceived = CompletableDeferred() + client.setNotificationHandler(Method.Defined.NotificationsMessage) { + notificationReceived.complete(it) + CompletableDeferred(Unit) + } + + val expectedData = JsonObject(mapOf("key" to JsonPrimitive("value"))) + + addTool("test-logging") { + sendLoggingMessage( + LoggingMessageNotification( + LoggingMessageNotificationParams( + level = expectedLevel, + data = expectedData, + ), + ), + ) + } + + client.callTool(CallToolRequest(CallToolRequestParams("test-logging"))) + val received = notificationReceived.await() + received.params.level shouldBe expectedLevel + received.params.data shouldBe expectedData + } + + @ParameterizedTest + @EnumSource(LoggingLevel::class) + fun `sendLoggingMessage should filter messages below level`(minLevel: LoggingLevel): Unit = runBlocking { + val receivedMessages = CopyOnWriteArrayList() + client.setNotificationHandler(Method.Defined.NotificationsMessage) { + receivedMessages.add(it) + CompletableDeferred(Unit) + } + + client.setLoggingLevel(minLevel) + + addTool("test-logging-level") { + LoggingLevel.entries.forEach { level -> + sendLoggingMessage( + LoggingMessageNotification( + LoggingMessageNotificationParams( + level = level, + data = JsonPrimitive(level.name), + ), + ), + ) + } + } + + client.callTool(CallToolRequest(CallToolRequestParams("test-logging-level"))) + + // Handlers dispatch on Dispatchers.Default after the handshake, so await the expected count + // before asserting order. + val expectedLevels = LoggingLevel.entries.filter { it >= minLevel } + eventually(5.seconds) { receivedMessages shouldHaveSize expectedLevels.size } + receivedMessages.map { it.params.level } shouldBe expectedLevels + } + + @Test + fun `sendLoggingMessage should send no messages when level is set to highest`(): Unit = runBlocking { + val receivedMessages = CopyOnWriteArrayList() + client.setNotificationHandler(Method.Defined.NotificationsMessage) { + receivedMessages.add(it) + CompletableDeferred(Unit) + } + + client.setLoggingLevel(LoggingLevel.Emergency) + + addTool("test-logging-highest") { + LoggingLevel.entries.dropLast(1).forEach { level -> + sendLoggingMessage( + LoggingMessageNotification( + LoggingMessageNotificationParams( + level = level, + data = JsonPrimitive(level.name), + ), + ), + ) + } + } + + client.callTool(CallToolRequest(CallToolRequestParams("test-logging-highest"))) + // Handlers dispatch asynchronously, so hold for a window to confirm nothing ever arrives. + continually(500.milliseconds) { receivedMessages.shouldBeEmpty() } + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionTest.kt new file mode 100644 index 0000000..581e5ae --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionTest.kt @@ -0,0 +1,178 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.withClue +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldNotBeBlank +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.ElicitationCompleteNotification +import io.modelcontextprotocol.kotlin.sdk.types.ElicitationCompleteNotificationParams +import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.ListRootsRequest +import io.modelcontextprotocol.kotlin.sdk.types.ListRootsResult +import io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel +import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification +import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotificationParams +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.Notification +import io.modelcontextprotocol.kotlin.sdk.types.PromptListChangedNotification +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.Request +import io.modelcontextprotocol.kotlin.sdk.types.RequestResult +import io.modelcontextprotocol.kotlin.sdk.types.ResourceListChangedNotification +import io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotification +import io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotificationParams +import io.modelcontextprotocol.kotlin.sdk.types.Root +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.ToolListChangedNotification +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.JsonPrimitive +import org.junit.jupiter.api.Test + +class ClientConnectionTest : AbstractServerFeaturesTest() { + + private inline fun onClientNotification(method: Method): CompletableDeferred { + val deferred = CompletableDeferred() + client.setNotificationHandler(method) { + deferred.complete(it) + CompletableDeferred(Unit) + } + return deferred + } + + private inline fun onClientRequest( + method: Method, + crossinline response: (Req) -> Res, + ) { + client.setRequestHandler(method) { req, _ -> response(req) } + } + + override fun getServerCapabilities(): ServerCapabilities = ServerCapabilities( + tools = ServerCapabilities.Tools(listChanged = true), + prompts = ServerCapabilities.Prompts(listChanged = true), + resources = ServerCapabilities.Resources(listChanged = true), + logging = ServerCapabilities.Logging, + ) + + override fun getClientCapabilities(): ClientCapabilities = ClientCapabilities( + roots = ClientCapabilities.Roots(listChanged = true), + elicitation = ClientCapabilities.Elicitation(url = EmptyJsonObject), + ) + + private val sampleRoots = listOf(Root("file:///project", "Project Root")) + + /** Holds all deferreds that capture what a server-side handler emits during [callAllMethods]. */ + private inner class Captures { + val pingComplete = CompletableDeferred() + val rootsResult = CompletableDeferred() + val sessionId = CompletableDeferred() + val toolListChanged = + onClientNotification(Method.Defined.NotificationsToolsListChanged) + val resourceListChanged = + onClientNotification(Method.Defined.NotificationsResourcesListChanged) + val promptListChanged = + onClientNotification(Method.Defined.NotificationsPromptsListChanged) + val loggingMessage = onClientNotification(Method.Defined.NotificationsMessage) + val resourceUpdated = + onClientNotification(Method.Defined.NotificationsResourcesUpdated) + val elicitationComplete = + onClientNotification(Method.Defined.NotificationsElicitationComplete) + + init { + onClientRequest(Method.Defined.RootsList) { + ListRootsResult(sampleRoots) + } + } + + suspend fun assertAll() { + val roots = rootsResult.await() + val log = loggingMessage.await() + val resourceUri = resourceUpdated.await().params.uri + val capturedSessionId = sessionId.await() + assertSoftly { + withClue("ping() was not called inside the handler") { + pingComplete.isCompleted shouldBe true + } + roots.roots shouldBe sampleRoots + withClue("sendToolListChanged() did not deliver a notification to the client") { + toolListChanged.isCompleted shouldBe true + } + withClue("sendResourceListChanged() did not deliver a notification to the client") { + resourceListChanged.isCompleted shouldBe true + } + withClue("sendPromptListChanged() did not deliver a notification to the client") { + promptListChanged.isCompleted shouldBe true + } + withClue("sendLoggingMessage() delivered unexpected payload") { + log.params.data shouldBe JsonPrimitive("log") + } + withClue("sendResourceUpdated() delivered wrong URI") { + resourceUri shouldBe "test://res" + } + withClue("sendElicitationComplete() did not deliver a notification to the client") { + elicitationComplete.isCompleted shouldBe true + } + withClue("sendElicitationComplete() delivered wrong elicitationId") { + elicitationComplete.await().params.elicitationId shouldBe "elicit-123" + } + capturedSessionId.shouldNotBeBlank() + } + } + } + + /** Exercises every [ClientConnection] method, recording results into [cap]. */ + private suspend fun ClientConnection.callAllMethods(cap: Captures) { + ping() + cap.pingComplete.complete(Unit) + cap.rootsResult.complete(listRoots()) + sendToolListChanged() + sendResourceListChanged() + sendPromptListChanged() + sendLoggingMessage( + LoggingMessageNotification( + LoggingMessageNotificationParams(level = LoggingLevel.Info, data = JsonPrimitive("log")), + ), + ) + sendResourceUpdated(ResourceUpdatedNotification(ResourceUpdatedNotificationParams("test://res"))) + sendElicitationComplete( + ElicitationCompleteNotification(ElicitationCompleteNotificationParams(elicitationId = "elicit-123")), + ) + cap.sessionId.complete(sessionId) + } + + @Test + fun `all ClientConnection methods are callable from tool handler`() = runTest { + val cap = Captures() + addTool("test") { callAllMethods(cap) } + + client.callTool(CallToolRequest(CallToolRequestParams("test"))) + + cap.assertAll() + } + + @Test + fun `all ClientConnection methods are callable from prompt handler`() = runTest { + val cap = Captures() + addPrompt("test") { callAllMethods(cap) } + + client.getPrompt(GetPromptRequest(GetPromptRequestParams("test"))) + + cap.assertAll() + } + + @Test + fun `all ClientConnection methods are callable from resource handler`() = runTest { + val cap = Captures() + addResource("test://resource") { callAllMethods(cap) } + + client.readResource(ReadResourceRequest(ReadResourceRequestParams("test://resource"))) + + cap.assertAll() + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingTest.kt new file mode 100644 index 0000000..f161d8e --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingTest.kt @@ -0,0 +1,251 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions +import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageResult +import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.IncludeContext +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.Role +import io.modelcontextprotocol.kotlin.sdk.types.SamplingMessage +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.StopReason +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.Tool +import io.modelcontextprotocol.kotlin.sdk.types.ToolChoice +import io.modelcontextprotocol.kotlin.sdk.types.ToolResultContent +import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema +import io.modelcontextprotocol.kotlin.sdk.types.ToolUseContent +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import org.junit.jupiter.api.assertDoesNotThrow +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class SamplingTest { + + private val dummyTool = Tool( + name = "t", + inputSchema = ToolSchema(properties = buildJsonObject { }, required = emptyList()), + ) + + private val weatherTool = Tool( + name = "get_weather", + description = "Return the current temperature in Celsius.", + inputSchema = ToolSchema( + properties = buildJsonObject { + put("location", buildJsonObject { put("type", JsonPrimitive("string")) }) + }, + required = listOf("location"), + ), + ) + + private val minimalMessages = listOf(SamplingMessage(Role.User, TextContent("hi"))) + + /** + * Builds a connected [Server]+[Client] pair using [InMemoryTransport]. + * + * @param clientCapabilities the capabilities the client advertises during initialize + * @param samplingHandler the handler the client uses to respond to sampling requests + * @return Pair of (server, sessionId) ready for [Server.createMessage] calls. + */ + private fun buildPair( + clientCapabilities: ClientCapabilities = ClientCapabilities( + sampling = ClientCapabilities.Sampling(), + ), + samplingHandler: (CreateMessageRequest) -> CreateMessageResult = { _ -> + CreateMessageResult(role = Role.Assistant, content = TextContent("ok"), model = "m") + }, + ): Pair { + val server = Server( + serverInfo = Implementation(name = "srv", version = "1.0"), + options = ServerOptions(capabilities = ServerCapabilities()), + ) + + val client = Client( + clientInfo = Implementation(name = "cli", version = "1.0"), + options = ClientOptions(capabilities = clientCapabilities), + ) + + client.setRequestHandler(Method.Defined.SamplingCreateMessage) { req, _ -> + samplingHandler(req) + } + + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + + var sessionId: String? = null + runBlocking { + val sessionDeferred = CompletableDeferred() + launch { client.connect(clientTransport) } + launch { + val session = server.createSession(serverTransport) + sessionDeferred.complete(session.sessionId) + } + sessionId = sessionDeferred.await() + } + + return Pair(server, checkNotNull(sessionId)) + } + + // ============================================================================ + // Server.createMessage — capability enforcement (SEP-1577) + // ============================================================================ + + @Test + fun `tools field rejected when client has no sampling tools capability`() { + val (server, sessionId) = buildPair() + assertFailsWith { + runBlocking { + server.createMessage( + sessionId = sessionId, + params = CreateMessageRequest( + params = CreateMessageRequestParams( + maxTokens = 100, + messages = minimalMessages, + tools = listOf(dummyTool), + ), + ), + ) + } + } + } + + @Test + fun `toolChoice field rejected when client has no sampling tools capability`() { + val (server, sessionId) = buildPair() + assertFailsWith { + runBlocking { + server.createMessage( + sessionId = sessionId, + params = CreateMessageRequest( + params = CreateMessageRequestParams( + maxTokens = 100, + messages = minimalMessages, + toolChoice = ToolChoice(), + ), + ), + ) + } + } + } + + @Test + fun `includeContext with no sampling context capability succeeds with a warning`() { + val (server, sessionId) = buildPair() + assertDoesNotThrow { + runBlocking { + server.createMessage( + sessionId = sessionId, + params = CreateMessageRequest( + params = CreateMessageRequestParams( + maxTokens = 100, + messages = minimalMessages, + includeContext = IncludeContext.ThisServer, + ), + ), + ) + } + } + } + + // ============================================================================ + // End-to-end tool-loop integration + // ============================================================================ + + @Test + fun `server sends tools, client returns tool_use then final text`() { + var turn = 0 + val (server, sessionId) = buildPair( + clientCapabilities = ClientCapabilities( + sampling = ClientCapabilities.Sampling(tools = EmptyJsonObject), + ), + samplingHandler = { _ -> + turn++ + if (turn == 1) { + CreateMessageResult( + role = Role.Assistant, + content = listOf( + TextContent("Let me check."), + ToolUseContent( + id = "call_1", + name = "get_weather", + input = buildJsonObject { put("location", JsonPrimitive("London")) }, + ), + ), + model = "test", + stopReason = StopReason.ToolUse, + ) + } else { + CreateMessageResult( + role = Role.Assistant, + content = TextContent("The temperature in London is 20°C."), + model = "test", + stopReason = StopReason.EndTurn, + ) + } + }, + ) + + runBlocking { + val messages = mutableListOf( + SamplingMessage(Role.User, TextContent("What is the weather in London?")), + ) + + // — Turn 1: server sends tools, expects tool_use stop reason + val first = server.createMessage( + sessionId = sessionId, + params = CreateMessageRequest( + params = CreateMessageRequestParams( + maxTokens = 256, + messages = messages.toList(), + tools = listOf(weatherTool), + toolChoice = ToolChoice(mode = ToolChoice.Mode.Auto), + ), + ), + ) + assertEquals(StopReason.ToolUse, first.stopReason) + assertEquals(2, first.content.size) + + // — Append assistant turn and inject tool result + messages.add(SamplingMessage(Role.Assistant, first.content)) + val toolUse = first.content.filterIsInstance().single() + messages.add( + SamplingMessage( + Role.User, + ToolResultContent( + toolUseId = toolUse.id, + content = listOf(TextContent("""{"tempC":20}""")), + ), + ), + ) + + // — Turn 2: server sends updated history, expects final text + val second = server.createMessage( + sessionId = sessionId, + params = CreateMessageRequest( + params = CreateMessageRequestParams( + maxTokens = 256, + messages = messages.toList(), + tools = listOf(weatherTool), + toolChoice = ToolChoice(mode = ToolChoice.Mode.Auto), + ), + ), + ) + assertEquals(StopReason.EndTurn, second.stopReason) + assertEquals(1, second.content.size) + val text = second.content.single() as TextContent + assertEquals("The temperature in London is 20°C.", text.text) + + server.close() + } + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerBulkFeaturesTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerBulkFeaturesTest.kt new file mode 100644 index 0000000..52f6908 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerBulkFeaturesTest.kt @@ -0,0 +1,257 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult +import io.modelcontextprotocol.kotlin.sdk.types.Prompt +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult +import io.modelcontextprotocol.kotlin.sdk.types.Resource +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.Tool +import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test + +class ServerBulkFeaturesTest : AbstractServerFeaturesTest() { + + override fun getServerCapabilities(): ServerCapabilities = ServerCapabilities( + tools = ServerCapabilities.Tools(listChanged = null), + prompts = ServerCapabilities.Prompts(listChanged = null), + resources = ServerCapabilities.Resources(listChanged = null, subscribe = null), + ) + + // ── addTools ─────────────────────────────────────────────────────────────── + + @Test + fun `addTools should register all provided tools`() = runTest { + server.addTools( + listOf( + RegisteredTool(Tool("bulk-a", ToolSchema(), "Tool A")) { CallToolResult(emptyList()) }, + RegisteredTool(Tool("bulk-b", ToolSchema(), "Tool B")) { CallToolResult(emptyList()) }, + RegisteredTool(Tool("bulk-c", ToolSchema(), "Tool C")) { CallToolResult(emptyList()) }, + ), + ) + + val tools = client.listTools().tools + + tools shouldHaveSize 3 + tools.map { it.name } shouldContainExactlyInAnyOrder listOf("bulk-a", "bulk-b", "bulk-c") + } + + @Test + fun `addTools should append to existing tools`() = runTest { + server.addTool("existing", "Existing") { CallToolResult(emptyList()) } + server.addTools( + listOf( + RegisteredTool(Tool("new-a", ToolSchema())) { CallToolResult(emptyList()) }, + RegisteredTool(Tool("new-b", ToolSchema())) { CallToolResult(emptyList()) }, + ), + ) + + val tools = client.listTools().tools + + tools shouldHaveSize 3 + tools.map { it.name } shouldContainExactlyInAnyOrder listOf("existing", "new-a", "new-b") + } + + @Test + fun `addTools should throw and register nothing when a tool is already registered`() = runTest { + server.addTool("existing", "Existing") { CallToolResult(emptyList()) } + + shouldThrow { + server.addTools( + listOf( + RegisteredTool(Tool("new-a", ToolSchema())) { CallToolResult(emptyList()) }, + RegisteredTool(Tool("existing", ToolSchema())) { CallToolResult(emptyList()) }, + ), + ) + } + + val tools = client.listTools().tools + + tools shouldHaveSize 1 + tools.map { it.name } shouldContainExactlyInAnyOrder listOf("existing") + } + + @Test + fun `addTools should throw when batch contains duplicate names`() = runTest { + shouldThrow { + server.addTools( + listOf( + RegisteredTool(Tool("dup", ToolSchema())) { CallToolResult(emptyList()) }, + RegisteredTool(Tool("dup", ToolSchema())) { CallToolResult(emptyList()) }, + ), + ) + } + + client.listTools().tools.shouldBeEmpty() + } + + // ── addPrompts ───────────────────────────────────────────────────────────── + + @Test + fun `addPrompts should register all provided prompts`() = runTest { + server.addPrompts( + listOf( + RegisteredPrompt(Prompt("bulk-p1", "Prompt 1")) { GetPromptResult(messages = emptyList()) }, + RegisteredPrompt(Prompt("bulk-p2", "Prompt 2")) { GetPromptResult(messages = emptyList()) }, + ), + ) + + val prompts = client.listPrompts().prompts + + prompts shouldHaveSize 2 + prompts.map { it.name } shouldContainExactlyInAnyOrder listOf("bulk-p1", "bulk-p2") + } + + @Test + fun `addPrompts should append to existing prompts`() = runTest { + server.addPrompt("existing-prompt", "Existing") { GetPromptResult(messages = emptyList()) } + server.addPrompts( + listOf( + RegisteredPrompt(Prompt("new-p")) { GetPromptResult(messages = emptyList()) }, + ), + ) + + val prompts = client.listPrompts().prompts + + prompts shouldHaveSize 2 + prompts.map { it.name } shouldContainExactlyInAnyOrder listOf("existing-prompt", "new-p") + } + + @Test + fun `addPrompts should throw and register nothing when a prompt is already registered`() = runTest { + server.addPrompt("existing-prompt", "Existing") { GetPromptResult(messages = emptyList()) } + + shouldThrow { + server.addPrompts( + listOf( + RegisteredPrompt(Prompt("new-p")) { GetPromptResult(messages = emptyList()) }, + RegisteredPrompt(Prompt("existing-prompt")) { GetPromptResult(messages = emptyList()) }, + ), + ) + } + + val prompts = client.listPrompts().prompts + + prompts shouldHaveSize 1 + prompts.map { it.name } shouldContainExactlyInAnyOrder listOf("existing-prompt") + } + + // ── addResources ─────────────────────────────────────────────────────────── + + @Test + fun `addResources should register all provided resources`() = runTest { + server.addResources( + listOf( + RegisteredResource(Resource("test://bulk-r1", "Resource 1")) { ReadResourceResult(emptyList()) }, + RegisteredResource(Resource("test://bulk-r2", "Resource 2")) { ReadResourceResult(emptyList()) }, + ), + ) + + val resources = client.listResources().resources + + resources shouldHaveSize 2 + resources.map { it.uri } shouldContainExactlyInAnyOrder listOf("test://bulk-r1", "test://bulk-r2") + } + + @Test + fun `addResources should append to existing resources`() = runTest { + server.addResource("test://existing-res", "Existing", "Existing resource") { + ReadResourceResult(emptyList()) + } + server.addResources( + listOf( + RegisteredResource(Resource("test://new-res", "New")) { ReadResourceResult(emptyList()) }, + ), + ) + + val resources = client.listResources().resources + + resources shouldHaveSize 2 + resources.map { it.uri } shouldContainExactlyInAnyOrder listOf("test://existing-res", "test://new-res") + } + + @Test + fun `addResources should throw and register nothing when a resource is already registered`() = runTest { + server.addResource("test://existing-res", "Existing", "Existing resource") { + ReadResourceResult(emptyList()) + } + + shouldThrow { + server.addResources( + listOf( + RegisteredResource(Resource("test://new-res", "New")) { ReadResourceResult(emptyList()) }, + RegisteredResource(Resource("test://existing-res", "Dup")) { ReadResourceResult(emptyList()) }, + ), + ) + } + + val resources = client.listResources().resources + + resources shouldHaveSize 1 + resources.map { it.uri } shouldContainExactlyInAnyOrder listOf("test://existing-res") + } + + // ── Partial-batch remove ─────────────────────────────────────────────────── + + @Test + fun `removeTools should return count of actually removed tools`() = runTest { + server.addTool("present", "Present") { CallToolResult(emptyList()) } + client.listTools().tools shouldHaveSize 1 + + val count = server.removeTools(listOf("present", "absent-1", "absent-2")) + + count shouldBe 1 + client.listTools().tools.shouldBeEmpty() + } + + @Test + fun `removeTools on empty list should return zero`() = runTest { + val count = server.removeTools(emptyList()) + + count shouldBe 0 + } + + @Test + fun `removePrompts should return count of actually removed prompts`() = runTest { + server.addPrompt("present-p", "Present") { GetPromptResult(messages = emptyList()) } + client.listPrompts().prompts shouldHaveSize 1 + + val count = server.removePrompts(listOf("present-p", "absent-p")) + + count shouldBe 1 + client.listPrompts().prompts.shouldBeEmpty() + } + + @Test + fun `removePrompts on empty list should return zero`() = runTest { + val count = server.removePrompts(emptyList()) + + count shouldBe 0 + } + + @Test + fun `removeResources should return count of actually removed resources`() = runTest { + server.addResource("test://present-r", "Present", "Present resource") { + ReadResourceResult(emptyList()) + } + client.listResources().resources shouldHaveSize 1 + + val count = server.removeResources(listOf("test://present-r", "test://absent-r")) + + count shouldBe 1 + client.listResources().resources.shouldBeEmpty() + } + + @Test + fun `removeResources on empty list should return zero`() = runTest { + val count = server.removeResources(emptyList()) + + count shouldBe 0 + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerFeaturesInvocationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerFeaturesInvocationTest.kt new file mode 100644 index 0000000..52db367 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerFeaturesInvocationTest.kt @@ -0,0 +1,222 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.kotest.assertions.assertSoftly +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.PromptMessage +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult +import io.modelcontextprotocol.kotlin.sdk.types.Role +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonPrimitive +import org.junit.jupiter.api.Test +import kotlin.test.assertFailsWith + +class ServerFeaturesInvocationTest : AbstractServerFeaturesTest() { + + override fun getServerCapabilities(): ServerCapabilities = ServerCapabilities( + tools = ServerCapabilities.Tools(listChanged = null), + prompts = ServerCapabilities.Prompts(listChanged = null), + resources = ServerCapabilities.Resources(listChanged = null, subscribe = null), + ) + + // ── Tool invocation ──────────────────────────────────────────────────────── + + @Test + fun `callTool should return tool result content`() = runTest { + server.addTool("echo", "Echo tool") { + CallToolResult(listOf(TextContent("echo response"))) + } + + val result = client.callTool(CallToolRequest(CallToolRequestParams("echo"))) + + assertSoftly(result) { + isError shouldNotBe true + (content.single() as TextContent).text shouldBe "echo response" + } + } + + @Test + fun `callTool should pass arguments to handler`() = runTest { + server.addTool("greet", "Greeting tool") { request -> + val name = request.params.arguments?.get("name")?.jsonPrimitive?.content ?: "stranger" + CallToolResult(listOf(TextContent("Hello, $name!"))) + } + + val result = client.callTool( + CallToolRequest( + CallToolRequestParams( + name = "greet", + arguments = JsonObject(mapOf("name" to JsonPrimitive("World"))), + ), + ), + ) + + (result.content.single() as TextContent).text shouldBe "Hello, World!" + } + + @Test + fun `callTool should return error result when tool not found`() = runTest { + val result = client.callTool(CallToolRequest(CallToolRequestParams("nonexistent"))) + + assertSoftly(result) { + isError shouldBe true + (content.single() as TextContent).text shouldBe "Tool nonexistent not found" + } + } + + @Test + fun `callTool should return error result when handler throws`() = runTest { + server.addTool("failing", "Failing tool") { + throw IllegalStateException("handler failure") + } + + val result = client.callTool(CallToolRequest(CallToolRequestParams("failing"))) + + assertSoftly(result) { + isError shouldBe true + (content.single() as TextContent).text shouldBe "Error executing tool failing: handler failure" + } + } + + @Test + fun `callTool should pass empty arguments map to handler`() = runTest { + server.addTool("greet", "Greeting tool") { request -> + val name = request.params.arguments?.get("name")?.jsonPrimitive?.content ?: "stranger" + CallToolResult(listOf(TextContent("Hello, $name!"))) + } + + val result = client.callTool( + CallToolRequest( + CallToolRequestParams( + name = "greet", + arguments = JsonObject(emptyMap()), + ), + ), + ) + + (result.content.single() as TextContent).text shouldBe "Hello, stranger!" + } + + // ── Prompt invocation ────────────────────────────────────────────────────── + + @Test + fun `getPrompt should return prompt description and messages`() = runTest { + server.addPrompt("my-prompt", "My prompt") { + GetPromptResult( + description = "Prompt result description", + messages = listOf( + PromptMessage(role = Role.User, content = TextContent("User message")), + ), + ) + } + + val result = client.getPrompt(GetPromptRequest(GetPromptRequestParams("my-prompt"))) + + assertSoftly(result) { + description shouldBe "Prompt result description" + messages shouldHaveSize 1 + (messages.single().content as TextContent).text shouldBe "User message" + } + } + + @Test + fun `getPrompt should pass arguments to handler`() = runTest { + server.addPrompt("templated", "Templated prompt") { request -> + val topic = request.params.arguments?.get("topic") ?: "unknown" + GetPromptResult( + messages = listOf( + PromptMessage(role = Role.User, content = TextContent("Tell me about $topic")), + ), + ) + } + + val result = client.getPrompt( + GetPromptRequest( + GetPromptRequestParams( + name = "templated", + arguments = mapOf("topic" to "Kotlin coroutines"), + ), + ), + ) + + (result.messages.single().content as TextContent).text shouldBe "Tell me about Kotlin coroutines" + } + + @Test + fun `getPrompt should use default when arguments are absent`() = runTest { + server.addPrompt("templated", "Templated prompt") { request -> + val topic = request.params.arguments?.get("topic") ?: "unknown" + GetPromptResult( + messages = listOf( + PromptMessage(role = Role.User, content = TextContent("Tell me about $topic")), + ), + ) + } + + val result = client.getPrompt(GetPromptRequest(GetPromptRequestParams("templated"))) + + (result.messages.single().content as TextContent).text shouldBe "Tell me about unknown" + } + + @Test + fun `getPrompt should throw when prompt not found`() = runTest { + assertFailsWith { + client.getPrompt(GetPromptRequest(GetPromptRequestParams("nonexistent"))) + } + } + + // ── Resource invocation ──────────────────────────────────────────────────── + + @Test + fun `readResource should return resource content`() = runTest { + val uri = "test://my-resource" + server.addResource(uri, "My Resource", "Test resource") { + ReadResourceResult( + contents = listOf(TextResourceContents(text = "resource content", uri = uri)), + ) + } + + val result = client.readResource(ReadResourceRequest(ReadResourceRequestParams(uri))) + + (result.contents.single() as TextResourceContents).text shouldBe "resource content" + } + + @Test + fun `readResource should pass request URI to handler`() = runTest { + val uri = "test://uri-check" + val receivedUri = CompletableDeferred() + server.addResource(uri, "URI Check", "Test") { request -> + receivedUri.complete(request.params.uri) + ReadResourceResult( + contents = listOf(TextResourceContents(text = "ok", uri = uri)), + ) + } + + client.readResource(ReadResourceRequest(ReadResourceRequestParams(uri))) + + receivedUri.await() shouldBe uri + } + + @Test + fun `readResource should throw when resource not found`() = runTest { + assertFailsWith { + client.readResource(ReadResourceRequest(ReadResourceRequestParams("test://nonexistent"))) + } + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerFeaturesListTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerFeaturesListTest.kt new file mode 100644 index 0000000..e6aca7f --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerFeaturesListTest.kt @@ -0,0 +1,221 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.kotest.assertions.assertSoftly +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult +import io.modelcontextprotocol.kotlin.sdk.types.ListResourceTemplatesRequest +import io.modelcontextprotocol.kotlin.sdk.types.PromptArgument +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult +import io.modelcontextprotocol.kotlin.sdk.types.Resource +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test + +class ServerFeaturesListTest : AbstractServerFeaturesTest() { + + override fun getServerCapabilities(): ServerCapabilities = ServerCapabilities( + tools = ServerCapabilities.Tools(listChanged = null), + prompts = ServerCapabilities.Prompts(listChanged = null), + resources = ServerCapabilities.Resources(listChanged = null, subscribe = null), + ) + + // ── listTools ────────────────────────────────────────────────────────────── + + @Test + fun `listTools should return empty list when no tools registered`() = runTest { + client.listTools().tools.shouldBeEmpty() + } + + @Test + fun `listTools should return all registered tools`() = runTest { + server.addTool("alpha", "First tool") { CallToolResult(emptyList()) } + server.addTool("beta", "Second tool") { CallToolResult(emptyList()) } + + val tools = client.listTools().tools + + tools shouldHaveSize 2 + tools.map { it.name } shouldContainExactlyInAnyOrder listOf("alpha", "beta") + } + + @Test + fun `listTools should expose tool name and description`() = runTest { + server.addTool("my-tool", "Detailed description") { CallToolResult(emptyList()) } + + val tool = client.listTools().tools.single() + + assertSoftly(tool) { + name shouldBe "my-tool" + description shouldBe "Detailed description" + } + } + + @Test + fun `listTools should reflect tool removal`() = runTest { + server.addTool("keep", "Kept") { CallToolResult(emptyList()) } + server.addTool("drop", "Dropped") { CallToolResult(emptyList()) } + client.listTools().tools shouldHaveSize 2 + + server.removeTool("drop") shouldBe true + + val tools = client.listTools().tools + + tools shouldHaveSize 1 + tools.single().name shouldBe "keep" + } + + @Test + fun `removeTool should return false when tool not found`() = runTest { + server.removeTool("ghost") shouldBe false + } + + // ── listPrompts ──────────────────────────────────────────────────────────── + + @Test + fun `listPrompts should return empty list when no prompts registered`() = runTest { + client.listPrompts().prompts.shouldBeEmpty() + } + + @Test + fun `listPrompts should return all registered prompts`() = runTest { + server.addPrompt("prompt-a", "First prompt") { GetPromptResult(messages = emptyList()) } + server.addPrompt("prompt-b", "Second prompt") { GetPromptResult(messages = emptyList()) } + + val prompts = client.listPrompts().prompts + + prompts shouldHaveSize 2 + prompts.map { it.name } shouldContainExactlyInAnyOrder listOf("prompt-a", "prompt-b") + } + + @Test + fun `listPrompts should expose prompt description`() = runTest { + server.addPrompt("my-prompt", "Helpful context prompt") { GetPromptResult(messages = emptyList()) } + + val prompt = client.listPrompts().prompts.single() + + assertSoftly(prompt) { + name shouldBe "my-prompt" + description shouldBe "Helpful context prompt" + } + } + + @Test + fun `listPrompts should include prompt arguments`() = runTest { + val args = listOf( + PromptArgument(name = "topic", description = "The topic to discuss", required = true), + PromptArgument(name = "tone", description = "Tone of voice", required = false), + ) + server.addPrompt("templated", "A templated prompt", args) { GetPromptResult(messages = emptyList()) } + + val prompt = client.listPrompts().prompts.single() + + val promptArgs = prompt.arguments.shouldNotBeNull() + promptArgs shouldHaveSize 2 + promptArgs.map { it.name } shouldContainExactlyInAnyOrder listOf("topic", "tone") + promptArgs.first { it.name == "topic" }.required shouldBe true + promptArgs.first { it.name == "tone" }.required shouldBe false + } + + @Test + fun `listPrompts should expose null description`() = runTest { + server.addPrompt("no-desc", null) { GetPromptResult(messages = emptyList()) } + + client.listPrompts().prompts.single().description shouldBe null + } + + @Test + fun `listPrompts should reflect prompt removal`() = runTest { + server.addPrompt("keep", "Kept") { GetPromptResult(messages = emptyList()) } + server.addPrompt("drop", "Dropped") { GetPromptResult(messages = emptyList()) } + client.listPrompts().prompts shouldHaveSize 2 + + server.removePrompt("drop") shouldBe true + + val prompts = client.listPrompts().prompts + + prompts shouldHaveSize 1 + prompts.single().name shouldBe "keep" + } + + @Test + fun `removePrompt should return false when prompt not found`() = runTest { + server.removePrompt("ghost") shouldBe false + } + + // ── listResources ────────────────────────────────────────────────────────── + + @Test + fun `listResources should return empty list when no resources registered`() = runTest { + client.listResources().resources.shouldBeEmpty() + } + + @Test + fun `listResources should return all registered resources`() = runTest { + server.addResource("test://res-1", "Resource 1", "First") { ReadResourceResult(emptyList()) } + server.addResource("test://res-2", "Resource 2", "Second") { ReadResourceResult(emptyList()) } + + val resources = client.listResources().resources + + resources shouldHaveSize 2 + resources.map { it.uri } shouldContainExactlyInAnyOrder listOf("test://res-1", "test://res-2") + } + + @Test + fun `listResources should expose resource metadata`() = runTest { + server.addResource("test://my-res", "My Resource", "A detailed resource", "application/json") { + ReadResourceResult(emptyList()) + } + + val resource = client.listResources().resources.single() + + assertSoftly(resource) { + uri shouldBe "test://my-res" + name shouldBe "My Resource" + description shouldBe "A detailed resource" + mimeType shouldBe "application/json" + } + } + + @Test + fun `listResources should expose resource without mimeType`() = runTest { + server.addResources( + listOf( + RegisteredResource(Resource("test://no-mime", "No Mime")) { + ReadResourceResult(emptyList()) + }, + ), + ) + + client.listResources().resources.single().mimeType shouldBe null + } + + @Test + fun `listResources should reflect resource removal`() = runTest { + server.addResource("test://keep", "Keep", "Kept") { ReadResourceResult(emptyList()) } + server.addResource("test://drop", "Drop", "Dropped") { ReadResourceResult(emptyList()) } + client.listResources().resources shouldHaveSize 2 + + server.removeResource("test://drop") shouldBe true + + val resources = client.listResources().resources + + resources shouldHaveSize 1 + resources.single().uri shouldBe "test://keep" + } + + @Test + fun `removeResource should return false when resource not found`() = runTest { + server.removeResource("test://ghost") shouldBe false + } + + // ── listResourceTemplates ────────────────────────────────────────────────── + + @Test + fun `listResourceTemplates should return empty list`() = runTest { + client.listResourceTemplates(ListResourceTemplatesRequest()).resourceTemplates.shouldBeEmpty() + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerHandlerNotificationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerHandlerNotificationTest.kt new file mode 100644 index 0000000..655d1f1 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerHandlerNotificationTest.kt @@ -0,0 +1,124 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult +import io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel +import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification +import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotificationParams +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import org.junit.jupiter.api.Test + +class ServerHandlerNotificationTest : AbstractServerFeaturesTest() { + + override fun getServerCapabilities(): ServerCapabilities = ServerCapabilities( + tools = ServerCapabilities.Tools(true), + prompts = ServerCapabilities.Prompts(true), + resources = ServerCapabilities.Resources(true, null), + logging = JsonObject(emptyMap()), + ) + + @Test + fun `tool handler can send notification to client`() = runTest { + val notificationReceived = CompletableDeferred() + client.setNotificationHandler(Method.Defined.NotificationsMessage) { notification -> + notificationReceived.complete(notification) + CompletableDeferred(Unit) + } + + val notificationText = "Notification from tool" + server.addTool("notify-tool", "A tool that notifies") { + sendLoggingMessage( + LoggingMessageNotification( + LoggingMessageNotificationParams( + level = LoggingLevel.Info, + data = JsonPrimitive(notificationText), + ), + ), + ) + CallToolResult(listOf(TextContent("Tool executed"))) + } + + val result = client.callTool(CallToolRequest(CallToolRequestParams(name = "notify-tool"))) + + (result.content[0] as TextContent).text shouldBe "Tool executed" + val received = notificationReceived.await() + received.params.level shouldBe LoggingLevel.Info + received.params.data shouldBe JsonPrimitive(notificationText) + } + + @Test + fun `prompt handler can send notification to client`() = runTest { + val notificationReceived = CompletableDeferred() + client.setNotificationHandler(Method.Defined.NotificationsMessage) { notification -> + notificationReceived.complete(notification) + CompletableDeferred(Unit) + } + + val notificationText = "Notification from prompt" + server.addPrompt("notify-prompt", "A prompt that notifies") { + sendLoggingMessage( + LoggingMessageNotification( + LoggingMessageNotificationParams( + level = LoggingLevel.Info, + data = JsonPrimitive(notificationText), + ), + ), + ) + GetPromptResult(messages = listOf(), description = "Prompt executed") + } + + val result = client.getPrompt(GetPromptRequest(GetPromptRequestParams(name = "notify-prompt"))) + + result.description shouldBe "Prompt executed" + val received = notificationReceived.await() + received.params.level shouldBe LoggingLevel.Info + received.params.data shouldBe JsonPrimitive(notificationText) + } + + @Test + fun `resource handler can send notification to client`() = runTest { + val notificationReceived = CompletableDeferred() + client.setNotificationHandler(Method.Defined.NotificationsMessage) { notification -> + notificationReceived.complete(notification) + CompletableDeferred(Unit) + } + + val resourceUri = "test://notify-resource" + val notificationText = "Notification from resource" + server.addResource(resourceUri, "Notify Resource", "A resource that notifies") { + sendLoggingMessage( + LoggingMessageNotification( + LoggingMessageNotificationParams( + level = LoggingLevel.Info, + data = JsonPrimitive(notificationText), + ), + ), + ) + ReadResourceResult( + contents = listOf(TextResourceContents(uri = resourceUri, text = "Resource read")), + ) + } + + val result = client.readResource(ReadResourceRequest(ReadResourceRequestParams(uri = resourceUri))) + + (result.contents[0] as TextResourceContents).text shouldBe "Resource read" + val received = notificationReceived.await() + received.params.level shouldBe LoggingLevel.Info + received.params.data shouldBe JsonPrimitive(notificationText) + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerInstructionsTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerInstructionsTest.kt new file mode 100644 index 0000000..c289b53 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerInstructionsTest.kt @@ -0,0 +1,84 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions +import io.modelcontextprotocol.kotlin.sdk.client.mcpClient +import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertNull +import kotlin.test.assertEquals + +@OptIn(ExperimentalMcpApi::class) +class ServerInstructionsTest { + + @Test + fun `Server constructor should accept instructions provider parameter`() = runTest { + val serverInfo = Implementation(name = "test server", version = "1.0") + val serverOptions = ServerOptions(capabilities = ServerCapabilities()) + val instructions = "This is a test server. Use it for testing purposes only." + + val server = Server(serverInfo, serverOptions, { instructions }) + + // The instructions should be stored internally and used in handleInitialize + // We can't directly access the private field, but we can test it through initialization + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + server.createSession(serverTransport) + + val client = mcpClient( + clientInfo = Implementation(name = "test client", version = "1.0"), + clientOptions = ClientOptions( + capabilities = ClientCapabilities( + roots = ClientCapabilities.Roots(listChanged = false), + ), + ), + transport = clientTransport, + ) + + assertEquals(instructions, client.serverInstructions) + } + + @Test + fun `Server constructor should accept instructions parameter`() = runTest { + val serverInfo = Implementation(name = "test server", version = "1.0") + val serverOptions = ServerOptions(capabilities = ServerCapabilities()) + val instructions = "This is a test server. Use it for testing purposes only." + + val server = Server(serverInfo, serverOptions, instructions) + + // The instructions should be stored internally and used in handleInitialize + // We can't directly access the private field, but we can test it through initialization + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + server.createSession(serverTransport) + + val client = mcpClient( + clientInfo = Implementation(name = "test client", version = "1.0"), + transport = clientTransport, + ) + + assertEquals(instructions, client.serverInstructions) + } + + @Test + fun `Server constructor should work without instructions parameter`() = runTest { + val serverInfo = Implementation(name = "test server", version = "1.0") + val serverOptions = ServerOptions(capabilities = ServerCapabilities()) + + // Test that server works when instructions parameter is omitted (defaults to null) + val server = Server(serverInfo, serverOptions) + + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + + server.createSession(serverTransport) + + val client = mcpClient( + clientInfo = Implementation(name = "test client", version = "1.0"), + transport = clientTransport, + ) + + assertNull(client.serverInstructions) + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerPromptsNotificationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerPromptsNotificationTest.kt new file mode 100644 index 0000000..08687d9 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerPromptsNotificationTest.kt @@ -0,0 +1,120 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.Prompt +import io.modelcontextprotocol.kotlin.sdk.types.PromptListChangedNotification +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.test.runTest +import org.awaitility.kotlin.await +import org.awaitility.kotlin.untilAsserted +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ServerPromptsNotificationTest : AbstractServerFeaturesTest() { + + override fun getServerCapabilities(): ServerCapabilities = ServerCapabilities( + prompts = ServerCapabilities.Prompts(true), + ) + + @Test + fun `addPrompt should send notification`() = runTest { + // Configure notification handler + val notifications = mutableListOf() + client.setNotificationHandler( + Method.Defined.NotificationsPromptsListChanged, + ) { notification -> + notifications.add(notification) + CompletableDeferred(Unit) + } + + // Add a prompt + val testPrompt = Prompt("test-prompt", "Test Prompt", null) + server.addPrompt(testPrompt) { + GetPromptResult( + description = "Test prompt description", + messages = listOf(), + ) + } + + // Remove the prompt + val result = server.removePrompt(testPrompt.name) + + // Verify the prompt was removed + assertTrue(result, "Prompt should be removed successfully") + + // Verify that the notification was sent + await untilAsserted { + assertTrue(notifications.isNotEmpty(), "Notification should be sent when prompt is added") + } + } + + @Test + fun `removePrompts should remove multiple prompts and send two notifications`() = runTest { + // Configure notification handler + val notifications = mutableListOf() + client.setNotificationHandler(Method.Defined.NotificationsPromptsListChanged) { + notifications.add(it) + CompletableDeferred(Unit) + } + + // Add prompts + val testPrompt1 = Prompt("test-prompt-1", "Test Prompt 1", null) + val testPrompt2 = Prompt("test-prompt-2", "Test Prompt 2", null) + + server.addPrompt(testPrompt1) { + GetPromptResult( + description = "Test prompt description 1", + messages = listOf(), + ) + } + server.addPrompt(testPrompt2) { + GetPromptResult( + description = "Test prompt description 2", + messages = listOf(), + ) + } + + // Remove the prompts + val result = server.removePrompts(listOf(testPrompt1.name, testPrompt2.name)) + + // Verify the prompts were removed + assertEquals(2, result, "Both prompts should be removed") + + // Verify that the notifications were sent twice + await untilAsserted { + assertEquals( + 4, + notifications.size, + "Two notifications should be sent when prompts are added and two when removed", + ) + } + } + + @Test + fun `notification should not be send when removed prompt does not exists`() = runTest { + // Track notifications + val notifications = mutableListOf() + client.setNotificationHandler( + Method.Defined.NotificationsPromptsListChanged, + ) { notification -> + notifications.add(notification) + CompletableDeferred(Unit) + } + + // Try to remove a non-existent prompt + val result = server.removePrompt("non-existent-prompt") + + // Verify the result + assertFalse(result, "Removing non-existent prompt should return false") + await untilAsserted { + assertTrue( + notifications.isEmpty(), + "No notification should be sent when prompt doesn't exist", + ) + } + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerPromptsTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerPromptsTest.kt new file mode 100644 index 0000000..44b1e42 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerPromptsTest.kt @@ -0,0 +1,165 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.kotest.matchers.string.shouldContain +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.Prompt +import io.modelcontextprotocol.kotlin.sdk.types.PromptListChangedNotification +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ServerPromptsTest : AbstractServerFeaturesTest() { + + override fun getServerCapabilities(): ServerCapabilities = ServerCapabilities( + prompts = ServerCapabilities.Prompts(null), + ) + + @Test + fun `removePrompt should remove a prompt and do not send notification`() = runTest { + // Configure notification handler + var promptListChangedNotificationReceived = false + client.setNotificationHandler(Method.Defined.NotificationsPromptsListChanged) { + promptListChangedNotificationReceived = true + CompletableDeferred(Unit) + } + + // Add a prompt + val testPrompt = Prompt("test-prompt", "Test Prompt", null) + server.addPrompt(testPrompt) { + GetPromptResult( + description = "Test prompt description", + messages = emptyList(), + ) + } + + // Remove the prompt + val result = server.removePrompt(testPrompt.name) + + // Verify the prompt was removed + assertTrue(result, "Prompt should be removed successfully") + + // Verify that the notification was not sent + assertFalse( + promptListChangedNotificationReceived, + "No notification should be sent when prompts capability is not supported", + ) + } + + @Test + fun `removePrompts should remove multiple prompts`() = runTest { + // Configure notification handler + var promptListChangedNotificationReceived = false + client.setNotificationHandler(Method.Defined.NotificationsPromptsListChanged) { + promptListChangedNotificationReceived = true + CompletableDeferred(Unit) + } + + // Add prompts + val testPrompt1 = Prompt("test-prompt-1", "Test Prompt 1", null) + val testPrompt2 = Prompt("test-prompt-2", "Test Prompt 2", null) + client.setNotificationHandler(Method.Defined.NotificationsPromptsListChanged) { + throw IllegalStateException("Notification should not be sent") + } + + server.addPrompt(testPrompt1) { + GetPromptResult( + description = "Test prompt description 1", + messages = emptyList(), + ) + } + server.addPrompt(testPrompt2) { + GetPromptResult( + description = "Test prompt description 2", + messages = emptyList(), + ) + } + + // Remove the prompts + val result = server.removePrompts(listOf(testPrompt1.name, testPrompt2.name)) + + // Verify the prompts were removed + assertEquals(2, result, "Both prompts should be removed") + + // Verify that the notification was not sent + assertFalse( + promptListChangedNotificationReceived, + "No notification should be sent when prompts capability is not supported", + ) + } + + @Test + fun `removePrompt should return false when prompt does not exist`() = runTest { + // Track notifications + var promptListChangedNotificationReceived = false + client.setNotificationHandler(Method.Defined.NotificationsPromptsListChanged) { + promptListChangedNotificationReceived = true + CompletableDeferred(Unit) + } + + // Try to remove a non-existent prompt + val result = server.removePrompt("non-existent-prompt") + + // Verify the result + assertFalse(result, "Removing non-existent prompt should return false") + assertFalse(promptListChangedNotificationReceived, "No notification should be sent when prompt doesn't exist") + } + + @Test + fun `addPrompt should throw when prompt with same name is already registered`() = runTest { + server.addPrompt(Prompt("test-prompt", "Original Prompt", null)) { + GetPromptResult(description = "original", messages = emptyList()) + } + + val exception = assertThrows { + server.addPrompt(Prompt("test-prompt", "Duplicate Prompt", null)) { + GetPromptResult(description = "duplicate", messages = emptyList()) + } + } + exception.message.orEmpty() shouldContain "Prompt \"test-prompt\" is already registered" + + // The original registration is intact + val prompts = client.listPrompts().prompts + assertEquals(1, prompts.size, "Only the original prompt should be registered") + val result = client.getPrompt(GetPromptRequest(GetPromptRequestParams("test-prompt"))) + assertEquals("original", result.description) + } + + @Test + fun `removePrompt should throw when prompts capability is not supported`() = runTest { + var promptListChangedNotificationReceived = false + client.setNotificationHandler(Method.Defined.NotificationsPromptsListChanged) { + promptListChangedNotificationReceived = true + CompletableDeferred(Unit) + } + + // Create server without prompts capability + val serverOptions = ServerOptions( + capabilities = ServerCapabilities(), + ) + val server = Server( + Implementation(name = "test server", version = "1.0"), + serverOptions, + ) + + // Verify that removing a prompt throws an exception + val exception = assertThrows { + server.removePrompt("test-prompt") + } + assertEquals("Server does not support prompts capability.", exception.message) + + // Verify that the notification was not sent + assertFalse( + promptListChangedNotificationReceived, + "No notification should be sent when prompts capability is not supported", + ) + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourceTemplateTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourceTemplateTest.kt new file mode 100644 index 0000000..8405469 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourceTemplateTest.kt @@ -0,0 +1,241 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.maps.shouldContainKey +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.ListResourceTemplatesRequest +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult +import io.modelcontextprotocol.kotlin.sdk.types.ResourceTemplate +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class ServerResourceTemplateTest : AbstractServerFeaturesTest() { + + override fun getServerCapabilities(): ServerCapabilities = ServerCapabilities( + resources = ServerCapabilities.Resources(listChanged = null, subscribe = null), + ) + + @Test + fun `listResourceTemplates should return registered templates`() = runTest { + server.addResourceTemplate("test://data/{id}", "Test Data", mimeType = "text/plain") { _, _ -> + ReadResourceResult(listOf(TextResourceContents("content", "test://data/1"))) + } + + val result = client.listResourceTemplates(ListResourceTemplatesRequest()) + + result.resourceTemplates shouldHaveSize 1 + result.resourceTemplates[0] shouldNotBeNull { + uriTemplate shouldBe "test://data/{id}" + name shouldBe "Test Data" + mimeType shouldBe "text/plain" + } + } + + @Test + fun `listResourceTemplates should return empty list when none registered`() = runTest { + val result = client.listResourceTemplates(ListResourceTemplatesRequest()) + + result.resourceTemplates.shouldBeEmpty() + } + + @Test + fun `readResource should match URI against template and invoke handler`() = runTest { + server.addResourceTemplate("test://items/{itemId}", "Item", mimeType = "text/plain") { request, variables -> + val itemId = variables["itemId"] ?: "unknown" + ReadResourceResult( + listOf(TextResourceContents(text = "item=$itemId", uri = request.uri, mimeType = "text/plain")), + ) + } + + val result = client.readResource(ReadResourceRequest(ReadResourceRequestParams("test://items/42"))) + + result.contents shouldBe + listOf(TextResourceContents(uri = "test://items/42", mimeType = "text/plain", text = "item=42")) + } + + @Test + fun `readResource should extract multiple URI template variables`() = runTest { + val capturedVars = CompletableDeferred>() + server.addResourceTemplate( + uriTemplate = "test://users/{userId}/posts/{postId}", + name = "User Post", + mimeType = "text/plain", + ) { _, variables -> + capturedVars.complete(variables) + ReadResourceResult(listOf(TextResourceContents("ok", "test://users/alice/posts/99"))) + } + + client.readResource(ReadResourceRequest(ReadResourceRequestParams("test://users/alice/posts/99"))) + + val vars = capturedVars.await() + vars shouldContainKey "userId" + vars shouldContainKey "postId" + vars["userId"] shouldBe "alice" + vars["postId"] shouldBe "99" + } + + @Test + fun `readResource should prefer exact resource match over template`() = runTest { + var exactHandlerCalled = false + server.addResource("test://items/special", "Special Item", "An exact resource") { + exactHandlerCalled = true + ReadResourceResult(listOf(TextResourceContents("exact", "test://items/special"))) + } + server.addResourceTemplate("test://items/{itemId}", "Item Template") { _, _ -> + ReadResourceResult(listOf(TextResourceContents("template", "test://items/special"))) + } + + val result = client.readResource(ReadResourceRequest(ReadResourceRequestParams("test://items/special"))) + + exactHandlerCalled shouldBe true + (result.contents[0] as TextResourceContents).text shouldBe "exact" + } + + @Test + fun `readResource should select most specific template when multiple match`() = runTest { + // "test://users/profile" has more literal chars than "test://users/{id}" — should win + server.addResourceTemplate("test://users/{id}", "Generic User") { _, variables -> + ReadResourceResult(listOf(TextResourceContents("generic:${variables["id"]}", "test://users/profile"))) + } + server.addResourceTemplate("test://users/profile", "Profile") { _, _ -> + ReadResourceResult(listOf(TextResourceContents("profile-page", "test://users/profile"))) + } + + val result = client.readResource(ReadResourceRequest(ReadResourceRequestParams("test://users/profile"))) + + (result.contents[0] as TextResourceContents).text shouldBe "profile-page" + } + + @Test + fun `readResource should return RESOURCE_NOT_FOUND error when no match`() = runTest { + val exception = assertThrows { + client.readResource(ReadResourceRequest(ReadResourceRequestParams("test://nonexistent/uri"))) + } + + exception.code shouldBe RPCError.ErrorCode.RESOURCE_NOT_FOUND + } + + @Test + fun `resourceTemplates property should reflect registered templates`() { + server.addResourceTemplate(ResourceTemplate("test://a/{x}", "A")) { _, _ -> + ReadResourceResult(emptyList()) + } + server.addResourceTemplate(ResourceTemplate("test://b/{y}", "B")) { _, _ -> + ReadResourceResult(emptyList()) + } + + val templates = server.resourceTemplates + + templates shouldBe listOf( + ResourceTemplate("test://a/{x}", "A"), + ResourceTemplate("test://b/{y}", "B"), + ) + } + + @Test + fun `removeResourceTemplate should remove a registered template`() { + server.addResourceTemplate("test://items/{id}", "Item") { _, _ -> + ReadResourceResult(emptyList()) + } + + val removed = server.removeResourceTemplate("test://items/{id}") + + removed shouldBe true + server.resourceTemplates.size shouldBe 0 + } + + @Test + fun `removeResourceTemplate should return false when template does not exist`() { + val removed = server.removeResourceTemplate("test://nonexistent/{id}") + + removed shouldBe false + } + + @Test + fun `addResourceTemplate should throw when template with same uriTemplate is already registered`() { + server.addResourceTemplate("test://items/{id}", "Item") { _, _ -> + ReadResourceResult(emptyList()) + } + + val exception = assertThrows { + server.addResourceTemplate("test://items/{id}", "Duplicate Item") { _, _ -> + ReadResourceResult(emptyList()) + } + } + exception.message.orEmpty() shouldContain "ResourceTemplate \"test://items/{id}\" is already registered" + + // The original registration is intact + server.resourceTemplates shouldHaveSize 1 + server.resourceTemplates[0].name shouldBe "Item" + } + + @Test + fun `addResourceTemplate should throw when resources capability is not supported`() { + val noResourcesServer = Server( + serverInfo = Implementation("test", "1.0"), + options = ServerOptions(capabilities = ServerCapabilities()), + ) + + assertThrows { + noResourcesServer.addResourceTemplate("test://{id}", "Test") { _, _ -> + ReadResourceResult(emptyList()) + } + } + } + + @Test + fun `addResourceTemplate with ResourceTemplate object should register correctly`() = runTest { + val template = ResourceTemplate( + uriTemplate = "test://docs/{section}", + name = "Documentation", + description = "API docs", + mimeType = "text/html", + ) + server.addResourceTemplate(template) { request, variables -> + val section = variables["section"] ?: "index" + ReadResourceResult( + listOf(TextResourceContents("docs for $section", request.uri, mimeType = "text/html")), + ) + } + + val result = client.readResource(ReadResourceRequest(ReadResourceRequestParams("test://docs/api"))) + + result.contents shouldHaveSize 1 + (result.contents[0] as TextResourceContents).text shouldBe "docs for api" + } + + @Test + fun `listResourceTemplates should include description from template`() = runTest { + server.addResourceTemplate( + uriTemplate = "test://data/{id}", + name = "Data", + description = "Parameterized data resource", + mimeType = "application/json", + ) { _, _ -> + ReadResourceResult(emptyList()) + } + + val result = client.listResourceTemplates(ListResourceTemplatesRequest()) + + result.resourceTemplates shouldBe listOf( + ResourceTemplate( + name = "Data", + uriTemplate = "test://data/{id}", + description = "Parameterized data resource", + mimeType = "application/json", + ), + ) + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourcesNotificationSubscribeTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourcesNotificationSubscribeTest.kt new file mode 100644 index 0000000..06fa962 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourcesNotificationSubscribeTest.kt @@ -0,0 +1,142 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.kotest.assertions.nondeterministic.eventually +import io.kotest.assertions.withClue +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsResourcesUpdated +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult +import io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotification +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.SubscribeRequest +import io.modelcontextprotocol.kotlin.sdk.types.SubscribeRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.runBlocking +import org.awaitility.kotlin.await +import org.awaitility.kotlin.untilAsserted +import org.junit.jupiter.api.Test +import java.util.concurrent.ConcurrentLinkedQueue +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +@OptIn(ExperimentalUuidApi::class) +class ServerResourcesNotificationSubscribeTest : AbstractServerFeaturesTest() { + + override fun getServerCapabilities(): ServerCapabilities = ServerCapabilities( + resources = ServerCapabilities.Resources(null, true), + ) + + @Test + fun `should send resource notifications`() = runBlocking { + val notifications = ConcurrentLinkedQueue() + + client.setNotificationHandler(NotificationsResourcesUpdated) { + notifications.add(it) + CompletableDeferred(Unit) + } + // Add resources + val testResourceUri1 = "test://resource1-${Uuid.random()}" + val testResourceUri2 = "test://resource2-${Uuid.random()}" + + client.subscribeResource(SubscribeRequest(SubscribeRequestParams(uri = testResourceUri1))) + client.subscribeResource(SubscribeRequest(SubscribeRequestParams(uri = testResourceUri2))) + + server.addResource( + uri = testResourceUri1, + name = "Test Resource 1", + description = "A test resource 1", + mimeType = "text/plain", + ) { request -> + ReadResourceResult( + contents = listOf( + TextResourceContents( + text = "Test resource content 1", + uri = request.uri, + mimeType = "text/plain", + ), + ), + ) + } + + server.addResource( + uri = testResourceUri2, + name = "Test Resource 2", + description = "A test resource 2", + mimeType = "text/plain", + ) { request -> + ReadResourceResult( + contents = listOf( + TextResourceContents( + text = "Test resource content 2", + uri = request.uri, + mimeType = "text/plain", + ), + ), + ) + } + + // Verify that the notification was sent + await untilAsserted { + notifications.shouldHaveSize(2) + assertTrue( + notifications.any { it.params.uri == testResourceUri1 && it.method == NotificationsResourcesUpdated }, + "Notification should be sent when resource 1 was deleted", + ) + assertTrue( + notifications.any { it.params.uri == testResourceUri2 && it.method == NotificationsResourcesUpdated }, + "Notification should be sent when resource 2 was deleted", + ) + } + + notifications.clear() + + // Remove the resource + val result1 = server.removeResource(testResourceUri1) + assertTrue(result1, "Resource 1 should be removed successfully") + + val result2 = server.removeResource(testResourceUri2) + // Verify the resource was removed + assertTrue(result2, "Resource 2 should be removed successfully") + + // Verify that the notification was sent + await untilAsserted { + notifications.shouldHaveSize(2) + assertTrue( + notifications.any { it.params.uri == testResourceUri1 && it.method == NotificationsResourcesUpdated }, + "Notification should be sent when resource 1 was deleted", + ) + assertTrue( + notifications.any { it.params.uri == testResourceUri2 && it.method == NotificationsResourcesUpdated }, + "Notification should be sent when resource 2 was deleted", + ) + } + } + + @Test + fun `notification should not be send when removed resource does not exists`(): Unit = runBlocking { + println("Thread ${Thread.currentThread().name} test 1") + // Track notifications + val notifications = ConcurrentLinkedQueue() + client.setNotificationHandler(NotificationsResourcesUpdated) { notification -> + notifications.add(notification) + CompletableDeferred(Unit) + } + + // Try to remove a non-existent resource + val result = server.removeResource("non-existent-resource") + withClue("Removing non-existent resource should return false") { + result shouldBe false + } + + // Verify no notification is sent (use eventually for cross-platform reliability) + withClue("No notification should be sent when resource doesn't exist") { + eventually(2.seconds) { + notifications.shouldBeEmpty() + } + } + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourcesNotificationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourcesNotificationTest.kt new file mode 100644 index 0000000..f007723 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourcesNotificationTest.kt @@ -0,0 +1,150 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult +import io.modelcontextprotocol.kotlin.sdk.types.ResourceListChangedNotification +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.test.runTest +import org.awaitility.kotlin.await +import org.awaitility.kotlin.untilAsserted +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ServerResourcesNotificationTest : AbstractServerFeaturesTest() { + + override fun getServerCapabilities(): ServerCapabilities = ServerCapabilities( + resources = ServerCapabilities.Resources(true, null), + ) + + @Test + fun `addResource should send notification`() = runTest { + // Configure notification handler + val notifications = mutableListOf() + client.setNotificationHandler( + Method.Defined.NotificationsResourcesListChanged, + ) { notification -> + notifications.add(notification) + CompletableDeferred(Unit) + } + + // Add a resource + val testResourceUri = "test://resource" + server.addResource( + uri = testResourceUri, + name = "Test Resource", + description = "A test resource", + mimeType = "text/plain", + ) { + ReadResourceResult( + contents = listOf( + TextResourceContents( + text = "Test resource content", + uri = testResourceUri, + mimeType = "text/plain", + ), + ), + ) + } + + // Remove the resource + val result = server.removeResource(testResourceUri) + + // Verify the resource was removed + assertTrue(result, "Resource should be removed successfully") + + // Verify that the notification was sent + await untilAsserted { + assertTrue(notifications.isNotEmpty(), "Notification should be sent when resource is added") + } + } + + @Test + fun `removeResources should remove multiple resources and send two notifications`() = runTest { + // Configure notification handler + val notifications = mutableListOf() + client.setNotificationHandler( + Method.Defined.NotificationsResourcesListChanged, + ) { + notifications.add(it) + CompletableDeferred(Unit) + } + + // Add resources + val testResourceUri1 = "test://resource1" + val testResourceUri2 = "test://resource2" + + server.addResource( + uri = testResourceUri1, + name = "Test Resource 1", + description = "A test resource 1", + mimeType = "text/plain", + ) { + ReadResourceResult( + contents = listOf( + TextResourceContents( + text = "Test resource content 1", + uri = testResourceUri1, + mimeType = "text/plain", + ), + ), + ) + } + server.addResource( + uri = testResourceUri2, + name = "Test Resource 2", + description = "A test resource 2", + mimeType = "text/plain", + ) { + ReadResourceResult( + contents = listOf( + TextResourceContents( + text = "Test resource content 2", + uri = testResourceUri2, + mimeType = "text/plain", + ), + ), + ) + } + + // Remove the resources + val result = server.removeResources(listOf(testResourceUri1, testResourceUri2)) + + // Verify the resources were removed + assertEquals(2, result, "Both resources should be removed") + + // Verify that the notifications were sent twice + await untilAsserted { + assertEquals( + 4, + notifications.size, + "Two notifications should be sent when resources are added and two when removed", + ) + } + } + + @Test + fun `notification should not be send when removed resource does not exists`() = runTest { + // Track notifications + val notifications = mutableListOf() + client.setNotificationHandler( + Method.Defined.NotificationsResourcesListChanged, + ) { + notifications.add(it) + CompletableDeferred(Unit) + } + + // Try to remove a non-existent resource + val result = server.removeResource("non-existent-resource") + + // Verify the result + assertFalse(result, "Removing non-existent resource should return false") + assertTrue( + notifications.isEmpty(), + "No notification should be sent when resource doesn't exist", + ) + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourcesTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourcesTest.kt new file mode 100644 index 0000000..a830788 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourcesTest.kt @@ -0,0 +1,156 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.kotest.matchers.string.shouldContain +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult +import io.modelcontextprotocol.kotlin.sdk.types.ResourceListChangedNotification +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ServerResourcesTest : AbstractServerFeaturesTest() { + + override fun getServerCapabilities(): ServerCapabilities = ServerCapabilities( + resources = ServerCapabilities.Resources(null, null), + ) + + @Test + fun `removeResource should remove a resource`() = runTest { + // Add a resource + val testResourceUri = "test://resource" + server.addResource( + uri = testResourceUri, + name = "Test Resource", + description = "A test resource", + mimeType = "text/plain", + ) { + ReadResourceResult( + contents = listOf( + TextResourceContents( + text = "Test resource content", + uri = testResourceUri, + mimeType = "text/plain", + ), + ), + ) + } + + // Remove the resource + val result = server.removeResource(testResourceUri) + + // Verify the resource was removed + assertTrue(result, "Resource should be removed successfully") + } + + @Test + fun `removeResources should remove multiple resources`() = runTest { + // Add resources + val testResourceUri1 = "test://resource1" + val testResourceUri2 = "test://resource2" + server.addResource( + uri = testResourceUri1, + name = "Test Resource 1", + description = "A test resource 1", + mimeType = "text/plain", + ) { + ReadResourceResult( + contents = listOf( + TextResourceContents( + text = "Test resource content 1", + uri = testResourceUri1, + mimeType = "text/plain", + ), + ), + ) + } + server.addResource( + uri = testResourceUri2, + name = "Test Resource 2", + description = "A test resource 2", + mimeType = "text/plain", + ) { + ReadResourceResult( + contents = listOf( + TextResourceContents( + text = "Test resource content 2", + uri = testResourceUri2, + mimeType = "text/plain", + ), + ), + ) + } + + // Remove the resources + val result = server.removeResources(listOf(testResourceUri1, testResourceUri2)) + + // Verify the resources were removed + assertEquals(2, result, "Both resources should be removed") + } + + @Test + fun `removeResource should return false when resource does not exist`() = runTest { + // Track notifications + var resourceListChangedNotificationReceived = false + client.setNotificationHandler( + Method.Defined.NotificationsResourcesListChanged, + ) { + resourceListChangedNotificationReceived = true + CompletableDeferred(Unit) + } + + // Try to remove a non-existent resource + val result = server.removeResource("non-existent-resource") + + // Verify the result + assertFalse(result, "Removing non-existent resource should return false") + assertFalse( + resourceListChangedNotificationReceived, + "No notification should be sent when resource doesn't exist", + ) + } + + @Test + fun `addResource should throw when resource with same uri is already registered`() = runTest { + server.addResource("test://resource", "Original", "Original resource") { + ReadResourceResult(listOf(TextResourceContents(text = "original", uri = "test://resource"))) + } + + val exception = assertThrows { + server.addResource("test://resource", "Duplicate", "Duplicate resource") { + ReadResourceResult(listOf(TextResourceContents(text = "duplicate", uri = "test://resource"))) + } + } + exception.message.orEmpty() shouldContain "Resource \"test://resource\" is already registered" + + // The original registration is intact + val result = client.readResource(ReadResourceRequest(ReadResourceRequestParams("test://resource"))) + assertEquals("original", (result.contents.single() as TextResourceContents).text) + } + + @Test + fun `removeResource should throw when resources capability is not supported`() = runTest { + // Create server without resources capability + val serverOptions = ServerOptions( + capabilities = ServerCapabilities(), + ) + val server = Server( + Implementation(name = "test server", version = "1.0"), + serverOptions, + ) + + // Verify that removing a resource throws an exception + val exception = assertThrows { + server.removeResource("test://resource") + } + assertEquals("Server does not support resources capability.", exception.message) + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSessionInitializeTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSessionInitializeTest.kt new file mode 100644 index 0000000..38b169a --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSessionInitializeTest.kt @@ -0,0 +1,194 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCError +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse +import io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION +import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import io.modelcontextprotocol.kotlin.sdk.types.RequestId +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.toJSON +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonObject +import org.junit.jupiter.api.Test +import java.util.concurrent.CopyOnWriteArrayList +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class ServerSessionInitializeTest { + + private fun createSession(): ServerSession = ServerSession( + serverInfo = Implementation(name = "test-server", version = "1.0"), + options = ServerOptions(capabilities = ServerCapabilities()), + instructions = null, + ) + + private fun createInitializeRequest(clientName: String = "test-client"): InitializeRequest = InitializeRequest( + InitializeRequestParams( + protocolVersion = LATEST_PROTOCOL_VERSION, + capabilities = ClientCapabilities(), + clientInfo = Implementation(name = clientName, version = "1.0"), + ), + ) + + private fun createMalformedInitializeRequest(): JSONRPCRequest = JSONRPCRequest( + id = RequestId(1), + method = "initialize", + params = buildJsonObject { + putJsonObject("capabilities") {} + putJsonObject("clientInfo") { + put("name", "repro") + put("version", "0.1.0") + } + }, + ) + + @Test + fun `should handle first initialize request successfully`() = runTest { + val session = createSession() + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + + assertNull(session.clientCapabilities) + assertNull(session.clientVersion) + + val responseDone = CompletableDeferred() + clientTransport.onMessage { message -> + if (message is JSONRPCResponse) { + responseDone.complete(message) + } + } + + session.connect(serverTransport) + clientTransport.send(createInitializeRequest().toJSON()) + + val response = responseDone.await() + assertNotNull(response.result) + assertNotNull(session.clientCapabilities) + assertEquals("test-client", session.clientVersion?.name) + } + + // Both initialize requests arrive before notifications/initialized, in the serial dispatch + // phase, so processing and response order stay deterministic. + @Test + fun `should classify malformed initialize params as invalid params`() = runTest { + val session = createSession() + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + + val responseDone = CompletableDeferred() + clientTransport.onMessage { message -> + if (message is JSONRPCError) { + responseDone.complete(message) + } + } + + session.connect(serverTransport) + clientTransport.send(createMalformedInitializeRequest()) + + val response = responseDone.await() + assertEquals(RPCError.ErrorCode.INVALID_PARAMS, response.error.code) + assertFalse(response.error.message.contains("kotlinx.serialization")) + assertNull(session.clientCapabilities) + assertNull(session.clientVersion) + } + + @Test + fun `should reject duplicate initialize request`() = runTest { + val session = createSession() + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + + val responses = CopyOnWriteArrayList() + val secondResponseDone = CompletableDeferred() + + clientTransport.onMessage { message -> + when (message) { + is JSONRPCResponse, is JSONRPCError -> { + responses.add(message) + if (responses.size == 2) secondResponseDone.complete(Unit) + } + + else -> {} + } + } + + session.connect(serverTransport) + + // First initialize should succeed + clientTransport.send(createInitializeRequest(clientName = "first-client").toJSON()) + + // Second initialize should be rejected + clientTransport.send(createInitializeRequest(clientName = "second-client").toJSON()) + + secondResponseDone.await() + + assertEquals(2, responses.size) + assertTrue(responses[0] is JSONRPCResponse, "First response should be success") + assertTrue(responses[1] is JSONRPCError, "Second response should be error") + assertEquals(RPCError.ErrorCode.INVALID_REQUEST, (responses[1] as JSONRPCError).error.code) + + // Capabilities still reflect the first client, not overwritten + assertEquals("first-client", session.clientVersion?.name) + } + + @Test + fun `should reject concurrent initialize requests - only first succeeds`() = runTest { + val session = createSession() + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + + val n = 10 + val allResponsesDone = CompletableDeferred() + val successes = CopyOnWriteArrayList() + val errors = CopyOnWriteArrayList() + + clientTransport.onMessage { message -> + when (message) { + is JSONRPCResponse -> successes.add(message) + is JSONRPCError -> errors.add(message) + else -> {} + } + if (successes.size + errors.size == n) { + allResponsesDone.complete(Unit) + } + } + + session.connect(serverTransport) + + // Use Dispatchers.Default for true parallelism on JVM + withContext(Dispatchers.Default) { + val barrier = CompletableDeferred() + val jobs = (1..n).map { i -> + launch { + barrier.await() + clientTransport.send( + createInitializeRequest(clientName = "client-$i").toJSON(), + ) + } + } + barrier.complete(Unit) + jobs.joinAll() + } + + allResponsesDone.await() + + assertEquals(1, successes.size, "Exactly one initialize should succeed") + assertEquals(n - 1, errors.size, "All other initializes should be rejected") + errors.forEach { error -> + assertEquals(RPCError.ErrorCode.INVALID_REQUEST, error.error.code) + } + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerToolsNotificationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerToolsNotificationTest.kt new file mode 100644 index 0000000..9ac744b --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerToolsNotificationTest.kt @@ -0,0 +1,126 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.ToolListChangedNotification +import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.test.runTest +import org.awaitility.kotlin.await +import org.awaitility.kotlin.untilAsserted +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ServerToolsNotificationTest : AbstractServerFeaturesTest() { + + override fun getServerCapabilities(): ServerCapabilities = ServerCapabilities( + tools = ServerCapabilities.Tools(true), + ) + + @Test + fun `addTool should send notification`() = runTest { + // Configure notification handler + val notifications = mutableListOf() + client.setNotificationHandler(Method.Defined.NotificationsToolsListChanged) { + notifications.add(it) + CompletableDeferred(Unit) + } + + // Add a tool + server.addTool("test-tool", "Test Tool", ToolSchema()) { + CallToolResult(listOf(TextContent("Test result"))) + } + + // Remove the tool + val result = server.removeTool("test-tool") + + // Verify the tool was removed + assertTrue(result, "Tool should be removed successfully") + + // Verify that the notification was sent + await untilAsserted { + assertTrue(notifications.isNotEmpty(), "Notification should be sent when tool is added") + } + } + + @Test + fun `removeTools should remove multiple tools and send two notifications`() = runTest { + // Configure notification handler + val notifications = mutableListOf() + client.setNotificationHandler( + Method.Defined.NotificationsToolsListChanged, + ) { notification -> + notifications.add(notification) + CompletableDeferred(Unit) + } + + // Add tools + server.addTool("test-tool-1", "Test Tool 1") { + CallToolResult(listOf(TextContent("Test result 1"))) + } + server.addTool("test-tool-2", "Test Tool 2") { + CallToolResult(listOf(TextContent("Test result 2"))) + } + + // Remove the tools + val result = server.removeTools(listOf("test-tool-1", "test-tool-2")) + // Verify the tools were removed + assertEquals(2, result, "Both tools should be removed") + + // Verify that the notifications were sent twice + await untilAsserted { + assertEquals( + 4, + notifications.size, + "Two notifications should be sent when tools are added and two when removed", + ) + } + } + + @Test + fun `addTool should not send notification when duplicate is rejected`() = runTest { + // Track notifications + val notifications = mutableListOf() + client.setNotificationHandler( + Method.Defined.NotificationsToolsListChanged, + ) { notification -> + notifications.add(notification) + CompletableDeferred(Unit) + } + + server.addTool("test-tool", "Test Tool") { CallToolResult(emptyList()) } + assertThrows { + server.addTool("test-tool", "Duplicate Tool") { CallToolResult(emptyList()) } + } + // Close the server to stop processing further events and flush notifications + server.close() + + assertEquals(1, notifications.size, "Only the first registration should send a notification") + } + + @Test + fun `notification should not be send when removed tool does not exists`() = runTest { + // Track notifications + val notifications = mutableListOf() + client.setNotificationHandler( + Method.Defined.NotificationsToolsListChanged, + ) { notification -> + notifications.add(notification) + CompletableDeferred(Unit) + } + + // Try to remove a non-existent tool + val result = server.removeTool("non-existent-tool") + // Close the server to stop processing further events and flush notifications + server.close() + + // Verify the result + assertFalse(result, "Removing non-existent tool should return false") + assertTrue(notifications.isEmpty(), "No notification should be sent when tool doesn't exist") + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerToolsTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerToolsTest.kt new file mode 100644 index 0000000..1b3df91 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerToolsTest.kt @@ -0,0 +1,179 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.kotest.matchers.string.shouldContain +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.ToolListChangedNotification +import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ServerToolsTest : AbstractServerFeaturesTest() { + + override fun getServerCapabilities(): ServerCapabilities = ServerCapabilities( + tools = ServerCapabilities.Tools(null), + ) + + @Test + fun `removeTool should remove a tool and do not send notification`() = runTest { + // Configure notification handler + var toolListChangedNotificationReceived = false + client.setNotificationHandler(Method.Defined.NotificationsToolsListChanged) { + toolListChangedNotificationReceived = true + CompletableDeferred(Unit) + } + + // Add a tool + server.addTool("test-tool", "Test Tool", ToolSchema()) { + CallToolResult(listOf(TextContent("Test result"))) + } + + // Remove the tool + val result = server.removeTool("test-tool") + + // Verify the tool was removed + assertTrue(result, "Tool should be removed successfully") + + // Verify that the notification was not sent + assertFalse( + toolListChangedNotificationReceived, + "No notification should be sent when tools capability is not supported", + ) + } + + @Test + fun `removeTools should remove multiple tools`() = runTest { + // Configure notification handler + var toolListChangedNotificationReceived = false + client.setNotificationHandler(Method.Defined.NotificationsToolsListChanged) { + toolListChangedNotificationReceived = true + CompletableDeferred(Unit) + } + + // Add tools + server.addTool("test-tool-1", "Test Tool 1") { + CallToolResult(listOf(TextContent("Test result 1"))) + } + server.addTool("test-tool-2", "Test Tool 2") { + CallToolResult(listOf(TextContent("Test result 2"))) + } + client.setNotificationHandler(Method.Defined.NotificationsToolsListChanged) { + throw IllegalStateException("Notification should not be sent") + } + + // Remove the tools + val result = server.removeTools(listOf("test-tool-1", "test-tool-2")) + + // Verify the tools were removed + assertEquals(2, result, "Both tools should be removed") + + // Verify that the notification was not sent + assertFalse( + toolListChangedNotificationReceived, + "No notification should be sent when tools capability is not supported", + ) + } + + @Test + fun `removeTool should return false when tool does not exist`() = runTest { + // Track notifications + var toolListChangedNotificationReceived = false + client.setNotificationHandler(Method.Defined.NotificationsToolsListChanged) { + toolListChangedNotificationReceived = true + CompletableDeferred(Unit) + } + + // Try to remove a non-existent tool + val result = server.removeTool("non-existent-tool") + + // Verify the result + assertFalse(result, "Removing non-existent tool should return false") + assertFalse(toolListChangedNotificationReceived, "No notification should be sent when tool doesn't exist") + } + + @Test + fun `addTool should succeed with non-conforming tool name`() = runTest { + server.addTool("my invalid tool!", "Tool with non-conforming name", ToolSchema()) { + CallToolResult(listOf(TextContent("It works"))) + } + + val result = server.removeTool("my invalid tool!") + assertTrue(result, "Tool with non-conforming name should have been registered and removable") + } + + @Test + fun `addTool should throw when tool with same name is already registered`() = runTest { + server.addTool("test-tool", "Original Tool") { + CallToolResult(listOf(TextContent("Original result"))) + } + + val exception = assertThrows { + server.addTool("test-tool", "Duplicate Tool") { + CallToolResult(listOf(TextContent("Duplicate result"))) + } + } + exception.message.orEmpty() shouldContain "Tool \"test-tool\" is already registered" + + // The original registration is intact + val tools = client.listTools().tools + assertEquals(1, tools.size, "Only the original tool should be registered") + val result = client.callTool(CallToolRequest(CallToolRequestParams("test-tool"))) + assertEquals("Original result", (result.content.single() as TextContent).text) + } + + @Test + fun `addTool should succeed after removing existing tool`() = runTest { + server.addTool("test-tool", "Original Tool") { + CallToolResult(listOf(TextContent("Original result"))) + } + + server.removeTool("test-tool") + server.addTool("test-tool", "Replacement Tool") { + CallToolResult(listOf(TextContent("Replacement result"))) + } + + assertEquals(1, client.listTools().tools.size, "Re-adding after removal should not duplicate the tool") + val result = client.callTool(CallToolRequest(CallToolRequestParams("test-tool"))) + assertEquals("Replacement result", (result.content.single() as TextContent).text) + } + + @Test + fun `removeTool should throw when tools capability is not supported`() = runTest { + var toolListChangedNotificationReceived = false + client.setNotificationHandler(Method.Defined.NotificationsToolsListChanged) { + toolListChangedNotificationReceived = true + CompletableDeferred(Unit) + } + + // Create server without tools capability + val serverOptions = ServerOptions( + capabilities = ServerCapabilities(), + ) + val server = Server( + Implementation(name = "test server", version = "1.0"), + serverOptions, + ) + + // Verify that removing a tool throws an exception + val exception = assertThrows { + server.removeTool("test-tool") + } + assertEquals("Server does not support tools capability.", exception.message) + + // Verify that the notification was not sent + assertFalse( + toolListChangedNotificationReceived, + "No notification should be sent when tools capability is not supported", + ) + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransportTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransportTest.kt new file mode 100644 index 0000000..4d798ce --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransportTest.kt @@ -0,0 +1,75 @@ +package io.modelcontextprotocol.kotlin.sdk.testing + +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.nulls.shouldNotBeNull +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest +import io.modelcontextprotocol.kotlin.sdk.types.InitializeResult +import io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION +import io.modelcontextprotocol.kotlin.sdk.types.ListResourcesRequest +import io.modelcontextprotocol.kotlin.sdk.types.ListResourcesResult +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.Resource +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import kotlinx.coroutines.runBlocking +import kotlin.test.Test + +@OptIn(ExperimentalMcpApi::class) +class ChannelTransportTest { + + @Test + fun `should connect and list resources`(): Unit = runBlocking { + val serverOptions = ServerOptions( + capabilities = ServerCapabilities( + resources = ServerCapabilities.Resources(), + ), + ) + val server = Server( + Implementation(name = "test server", version = "1.0"), + serverOptions, + ) + + val (clientTransport, serverTransport) = ChannelTransport.createLinkedPair() + + val client = Client( + clientInfo = Implementation(name = "test client", version = "1.0"), + ) + + val serverSession = server.createSession(serverTransport) + client.connect(clientTransport) + + serverSession.setRequestHandler(Method.Defined.Initialize) { _, _ -> + InitializeResult( + protocolVersion = LATEST_PROTOCOL_VERSION, + capabilities = ServerCapabilities( + resources = ServerCapabilities.Resources(null, null), + tools = ServerCapabilities.Tools(null), + ), + serverInfo = Implementation(name = "test", version = "1.0"), + ) + } + + serverSession.setRequestHandler(Method.Defined.ResourcesList) { _, _ -> + ListResourcesResult( + resources = listOf( + Resource( + uri = "/foo/bar", + name = "foo-bar-resource", + ), + ), + ) + } + + // These should not throw + client.listResources() shouldNotBeNull { + this.resources shouldHaveSize 1 + } + + client.close() + server.close() + } +} diff --git a/integration-test/src/jvmTest/resources/junit-platform.properties b/integration-test/src/jvmTest/resources/junit-platform.properties new file mode 100644 index 0000000..9af7987 --- /dev/null +++ b/integration-test/src/jvmTest/resources/junit-platform.properties @@ -0,0 +1,6 @@ +## https://docs.junit.org/5.3.0-M1/user-guide/index.html#writing-tests-parallel-execution +junit.jupiter.execution.parallel.enabled=true +junit.jupiter.execution.parallel.config.strategy=dynamic +junit.jupiter.execution.parallel.mode.default=concurrent +junit.jupiter.execution.parallel.mode.classes.default=concurrent +junit.jupiter.execution.timeout.default=2m diff --git a/integration-test/src/jvmTest/resources/simplelogger.properties b/integration-test/src/jvmTest/resources/simplelogger.properties new file mode 100644 index 0000000..95fb646 --- /dev/null +++ b/integration-test/src/jvmTest/resources/simplelogger.properties @@ -0,0 +1,8 @@ +# Level of logging for the ROOT logger: ERROR, WARN, INFO, DEBUG, TRACE (default is INFO) +org.slf4j.simpleLogger.defaultLogLevel=INFO +org.slf4j.simpleLogger.showThreadName=true +org.slf4j.simpleLogger.showDateTime=false + +# Log level for specific packages or classes +org.slf4j.simpleLogger.log.io.ktor.server=DEBUG +org.slf4j.simpleLogger.log.io.modelcontextprotocol=DEBUG diff --git a/integration-test/src/jvmTest/typescript/client/http-client.ts b/integration-test/src/jvmTest/typescript/client/http-client.ts new file mode 100644 index 0000000..5956a37 --- /dev/null +++ b/integration-test/src/jvmTest/typescript/client/http-client.ts @@ -0,0 +1,93 @@ +import {Client} from '@modelcontextprotocol/sdk/client/index'; +import {StreamableHTTPClientTransport} from '@modelcontextprotocol/sdk/client/streamableHttp'; + +const args = process.argv.slice(2); +const serverUrl = args[0] || 'http://localhost:3000/mcp'; +const toolName = args[1]; +const toolArgs = args.slice(2); +const PROTOCOL_VERSION = "2025-11-25"; + +async function main() { + if (!toolName) { + console.log('Usage: npx tsx client/http-client.ts [server-url] [tool-args...]'); + console.log('Using default server URL:', serverUrl); + console.log('Available utils will be listed after connection'); + } + + console.log(`Connecting to server at ${serverUrl}`); + if (toolName) { + console.log(`Will call tool: ${toolName} with args: ${toolArgs.join(', ')}`); + } + + const client = new Client({ + name: 'test-client', + version: '1.0.0' + }, { + fallbackNotificationHandler: async (notification) => { + console.log('Notification:', notification.method, JSON.stringify(notification)); + } + }); + + const transport = new StreamableHTTPClientTransport(new URL(serverUrl)); + + try { + await client.connect(transport, {protocolVersion: PROTOCOL_VERSION}); + console.log('Connected to server'); + + const toolsResult = await client.listTools(); + const tools = toolsResult.tools; + console.log('Available utils:', tools.map((t) => t.name).join(', ')); + + if (!toolName) { + return; + } + + const tool = tools.find((t) => t.name === toolName); + if (!tool) { + throw new Error(`Tool "${toolName}" not found`); + } + + const toolArguments: Record = {}; + + if (toolName === "greet" && toolArgs.length > 0) { + toolArguments["name"] = toolArgs[0]; + } else if (tool.inputSchema && tool.inputSchema.properties) { + const propNames = Object.keys(tool.inputSchema.properties); + if (propNames.length > 0 && toolArgs.length > 0) { + toolArguments[propNames[0]] = toolArgs[0]; + } + } + + console.log(`Calling tool ${toolName} with arguments:`, toolArguments); + + const result = await client.callTool({ + name: toolName, + arguments: toolArguments + }); + console.log('Tool result:', result); + + if (result.content) { + for (const content of result.content) { + if (content.type === 'text') { + console.log('Text content:', content.text); + } + } + } + + if (result.structuredContent) { + console.log('Structured content:', JSON.stringify(result.structuredContent, null, 2)); + } + + } catch (error) { + console.error('Error:', error); + process.exitCode = 1; + } finally { + await client.close(); + console.log('Disconnected from server'); + } +} + +main().catch(error => { + console.error('Unhandled error:', error); + process.exit(1); +}); diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/myClient.ts b/integration-test/src/jvmTest/typescript/client/stdio-client.ts similarity index 71% rename from kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/myClient.ts rename to integration-test/src/jvmTest/typescript/client/stdio-client.ts index e4aae91..a9c765c 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/myClient.ts +++ b/integration-test/src/jvmTest/typescript/client/stdio-client.ts @@ -1,24 +1,11 @@ -// @ts-nocheck +import {Client} from '@modelcontextprotocol/sdk/client/index'; + const args = process.argv.slice(2); const toolName = args[0]; const toolArgs = args.slice(1); -const PROTOCOL_VERSION = "2024-11-05"; +const PROTOCOL_VERSION = "2025-11-25"; async function main() { - const sdkDirRaw = process.env.TYPESCRIPT_SDK_DIR; - const sdkDir = sdkDirRaw ? sdkDirRaw.trim() : undefined; - - let Client: any; - - if (!sdkDir) { - throw new Error('TYPESCRIPT_SDK_DIR environment variable is not set. It should point to the cloned typescript-sdk directory.'); - } - - const path = await import('path'); - const { pathToFileURL } = await import('url'); - const clientUrl = pathToFileURL(path.join(sdkDir, 'src', 'client', 'index.ts')).href; - ({ Client } = await import(clientUrl)); - if (!toolName) { console.error('Available utils will be listed after connection'); } else { @@ -68,36 +55,28 @@ async function main() { async close(): Promise { this.closed = true; - try { process.stdin.pause(); } catch {} + try { + process.stdin.pause(); + } catch { + } } } const client = new Client({ name: 'test-client', version: '1.0.0' + }, { + fallbackNotificationHandler: async (notification) => { + console.error('Notification:', notification.method, JSON.stringify(notification)); + } }); const transport = new MinimalStdioClientTransport(); try { - await client.connect(transport, { protocolVersion: PROTOCOL_VERSION }); + await client.connect(transport, {protocolVersion: PROTOCOL_VERSION}); console.error('Connected to server over stdio'); - try { - if (typeof (client as any).on === 'function') { - (client as any).on('notification', (n: any) => { - try { - const method = (n && (n.method || (n.params && n.params.method))) || 'unknown'; - console.error('Notification:', method, JSON.stringify(n)); - } catch { - console.error('Notification: '); - } - }); - } - } catch { - // ignore - } - const toolsResult = await client.listTools(); const tools = toolsResult.tools; console.error('Available utils:', tools.map((t: { name: any; }) => t.name).join(', ')); @@ -109,16 +88,17 @@ async function main() { const tool = tools.find((t: { name: string; }) => t.name === toolName); if (!tool) { - console.error(`Tool "${toolName}" not found`); - process.exit(1); + // Don't call process.exit() here: it prevents finally{} from running, + // which breaks tests that expect a clean disconnect signal. + throw new Error(`Tool "${toolName}" not found`); } const toolArguments: any = {}; if (toolName === 'greet' && toolArgs.length > 0) { toolArguments['name'] = toolArgs[0]; - } else if (tool.input && tool.input.properties) { - const propNames = Object.keys(tool.input.properties); + } else if (tool.inputSchema && tool.inputSchema.properties) { + const propNames = Object.keys(tool.inputSchema.properties); if (propNames.length > 0 && toolArgs.length > 0) { toolArguments[propNames[0]] = toolArgs[0]; } @@ -146,7 +126,8 @@ async function main() { } catch (error) { console.error('Error:', error); - process.exit(1); + // Don't hard-exit; allow finally{} to run so the client always closes cleanly. + process.exitCode = 1; } finally { await client.close(); console.error('Disconnected from server'); diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json new file mode 100644 index 0000000..ffb53fc --- /dev/null +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -0,0 +1,2187 @@ +{ + "name": "mcp-typescript-test-sdk", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mcp-typescript-test-sdk", + "version": "1.0.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.30.0", + "express": "^5.2.1", + "zod": "^4.6.2" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@types/node": "^26.5.1", + "cors": "^2.8.6", + "tsx": "^4.23.13", + "typescript": "^7.0.2" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.0.tgz", + "integrity": "sha512-XovyyCCnBzW+zKu+z/zq8hwNs4KOR5rEMAOxo2f40Q5xoOI37IMm6MIg2COOUtUApo0i6850MTBKH2u4QLGIqg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9 || ^2.0.5", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.5.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.5.1.tgz", + "integrity": "sha512-CzNm2FezW4VR/LjG6yUdiEgLE/rAQ9Slj5gCu/C2VrdcW7I0ahNZ8DRbHT7zOZ6r3ONgd/bsQIeSaoDGrd1C6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.0.tgz", + "integrity": "sha512-XKJXDsASUOo0LLtFwW5hCcQGH0N4WQc/Rn8/Pvoia+TJFOkkFPvrtW9lZOeeNcxQJspvOIERMwiRLsVFlhHEkA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.7.tgz", + "integrity": "sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz", + "integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", + "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.23.13", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz", + "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.2.tgz", + "integrity": "sha512-lh5RCAGFa1Cm2hjtNwLQhSs/AsqdWnTQaBER9fEwN/88pSh7KOtJavtBx/0VlkN/uFd61SwYmljLMDAsHlvzBQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + } + } +} diff --git a/integration-test/src/jvmTest/typescript/package.json b/integration-test/src/jvmTest/typescript/package.json new file mode 100644 index 0000000..b0147e4 --- /dev/null +++ b/integration-test/src/jvmTest/typescript/package.json @@ -0,0 +1,24 @@ +{ + "name": "mcp-typescript-test-sdk", + "version": "1.0.0", + "private": true, + "scripts": { + "client:stdio": "tsx client/stdio-client.ts", + "client:http": "tsx client/http-client.ts http://localhost:3000/mcp greet Hello", + "server:stdio": "tsx server/stdio-server.ts", + "server:http": "tsx server/http-server.ts" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.30.0", + "express": "^5.2.1", + "zod": "^4.6.2" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@types/node": "^26.5.1", + "cors": "^2.8.6", + "tsx": "^4.23.13", + "typescript": "^7.0.2" + } +} diff --git a/integration-test/src/jvmTest/typescript/server/http-server.ts b/integration-test/src/jvmTest/typescript/server/http-server.ts new file mode 100644 index 0000000..c42e115 --- /dev/null +++ b/integration-test/src/jvmTest/typescript/server/http-server.ts @@ -0,0 +1,275 @@ +import express, {Request, Response} from 'express'; +import {randomUUID} from 'node:crypto'; +import cors from 'cors'; +import {McpServer} from '@modelcontextprotocol/sdk/server/mcp'; +import {StreamableHTTPServerTransport} from '@modelcontextprotocol/sdk/server/streamableHttp'; +import {registerTestUtils} from './server-common'; +import { + getOAuthProtectedResourceMetadataUrl, + mcpAuthMetadataRouter +} from '@modelcontextprotocol/sdk/server/auth/router'; +import {requireBearerAuth} from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth'; +import {isInitializeRequest} from '@modelcontextprotocol/sdk/types'; +import {InMemoryEventStore} from '@modelcontextprotocol/sdk/examples/shared/inMemoryEventStore'; +import {setupAuthServer} from '@modelcontextprotocol/sdk/examples/server/demoInMemoryOAuthProvider'; +import {checkResourceAllowed} from '@modelcontextprotocol/sdk/shared/auth-utils'; +import {OAuthMetadata} from "@modelcontextprotocol/sdk/shared/auth"; + +async function main() { + const useOAuth = process.argv.includes('--oauth'); + const strictOAuth = process.argv.includes('--oauth-strict'); + + const getServer = () => { + const server = new McpServer({ + name: 'simple-streamable-http-server', + version: '1.0.0' + }, {capabilities: {logging: {}}}); + + registerTestUtils(server); + + return server; + }; + + const MCP_PORT = process.env.MCP_PORT ? parseInt(process.env.MCP_PORT, 10) : 3000; + const AUTH_PORT = process.env.MCP_AUTH_PORT ? parseInt(process.env.MCP_AUTH_PORT, 10) : 3001; + + const app = express(); + app.use(express.json()); + + // Allow CORS all domains, expose the Mcp-Session-Id header + app.use(cors({ + origin: '*', + exposedHeaders: ["Mcp-Session-Id"] + })); + + // Set up OAuth if enabled + let authMiddleware = null; + if (useOAuth) { + const mcpServerUrl = new URL(`http://localhost:${MCP_PORT}/mcp`); + const authServerUrl = new URL(`http://localhost:${AUTH_PORT}`); + + const oauthMetadata: OAuthMetadata = setupAuthServer({ + authServerUrl, + mcpServerUrl, + strictResource: strictOAuth + }); + + const tokenVerifier = { + verifyAccessToken: async (token: string) => { + const endpoint = oauthMetadata.introspection_endpoint; + + if (!endpoint) { + throw new Error('No token verification endpoint available in metadata'); + } + + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + token: token + }).toString() + }); + + if (!response.ok) { + throw new Error(`Invalid or expired token: ${await response.text()}`); + } + + const data = await response.json(); + + if (strictOAuth) { + if (!data.aud) { + throw new Error(`Resource Indicator (RFC8707) missing`); + } + if (!checkResourceAllowed({requestedResource: data.aud, configuredResource: mcpServerUrl})) { + throw new Error(`Expected resource indicator ${mcpServerUrl}, got: ${data.aud}`); + } + } + + return { + token, + clientId: data.client_id, + scopes: data.scope ? data.scope.split(' ') : [], + expiresAt: data.exp, + }; + } + } + + app.use(mcpAuthMetadataRouter({ + oauthMetadata, + resourceServerUrl: mcpServerUrl, + scopesSupported: ['mcp:tools'], + resourceName: 'MCP Demo Server', + })); + + authMiddleware = requireBearerAuth({ + verifier: tokenVerifier, + requiredScopes: [], + resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl), + }); + } + + // Map to store transports by session ID + const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; + + // MCP POST endpoint with optional auth + const mcpPostHandler = async (req: Request, res: Response) => { + const sessionId = req.headers['mcp-session-id'] as string | undefined; + if (sessionId) { + console.log(`Received MCP request for session: ${sessionId}`); + } else { + console.log('Request body:', req.body); + } + + if (useOAuth && req.auth) { + console.log('Authenticated user:', req.auth); + } + try { + let transport: StreamableHTTPServerTransport; + if (sessionId && transports[sessionId]) { + transport = transports[sessionId]; + } else if (!sessionId && isInitializeRequest(req.body)) { + const eventStore = new InMemoryEventStore(); + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + eventStore, + onsessioninitialized: (sessionId) => { + console.log(`Session initialized with ID: ${sessionId}`); + transports[sessionId] = transport; + } + }); + + transport.onclose = () => { + const sid = transport.sessionId; + if (sid && transports[sid]) { + console.log(`Transport closed for session ${sid}, removing from transports map`); + delete transports[sid]; + } + }; + + const server = getServer(); + await server.connect(transport); + + await transport.handleRequest(req, res, req.body); + return; + } else { + res.status(400).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Bad Request: No valid session ID provided', + }, + id: null, + }); + return; + } + + await transport.handleRequest(req, res, req.body); + } catch (error) { + console.error('Error handling MCP request:', error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Internal server error', + }, + id: null, + }); + } + } + }; + + if (useOAuth && authMiddleware) { + app.post('/mcp', authMiddleware, mcpPostHandler); + } else { + app.post('/mcp', mcpPostHandler); + } + + // Handle GET requests for event streams + const mcpGetHandler = async (req: Request, res: Response) => { + const sessionId = req.headers['mcp-session-id'] as string | undefined; + if (!sessionId || !transports[sessionId]) { + res.status(400).send('Invalid or missing session ID'); + return; + } + + if (useOAuth && req.auth) { + console.log('Authenticated event stream connection from user:', req.auth); + } + + const lastEventId = req.headers['last-event-id'] as string | undefined; + if (lastEventId) { + console.log(`Client reconnecting with Last-Event-ID: ${lastEventId}`); + } else { + console.log(`Establishing new event stream for session ${sessionId}`); + } + + const transport = transports[sessionId]; + await transport.handleRequest(req, res); + }; + + if (useOAuth && authMiddleware) { + app.get('/mcp', authMiddleware, mcpGetHandler); + } else { + app.get('/mcp', mcpGetHandler); + } + + // Handle DELETE requests for session termination (according to MCP spec) + const mcpDeleteHandler = async (req: Request, res: Response) => { + const sessionId = req.headers['mcp-session-id'] as string | undefined; + if (!sessionId || !transports[sessionId]) { + res.status(400).send('Invalid or missing session ID'); + return; + } + + console.log(`Received session termination request for session ${sessionId}`); + + try { + const transport = transports[sessionId]; + await transport.handleRequest(req, res); + } catch (error) { + console.error('Error handling session termination:', error); + if (!res.headersSent) { + res.status(500).send('Error processing session termination'); + } + } + }; + + if (useOAuth && authMiddleware) { + app.delete('/mcp', authMiddleware, mcpDeleteHandler); + } else { + app.delete('/mcp', mcpDeleteHandler); + } + + app.listen(MCP_PORT, (error) => { + if (error) { + console.error('Failed to start server:', error); + process.exit(1); + } + console.log(`MCP Streamable HTTP Server listening on port ${MCP_PORT}`); + }); + + // Handle server shutdown + process.on('SIGINT', async () => { + console.log('Shutting down server...'); + + for (const sessionId in transports) { + try { + console.log(`Closing transport for session ${sessionId}`); + await transports[sessionId].close(); + delete transports[sessionId]; + } catch (error) { + console.error(`Error closing transport for session ${sessionId}:`, error); + } + } + console.log('Server shutdown complete'); + process.exit(0); + }); +} + +main().catch((err) => { + console.error('Failed to start server:', err); + process.exit(1); +}); diff --git a/integration-test/src/jvmTest/typescript/server/server-common.ts b/integration-test/src/jvmTest/typescript/server/server-common.ts new file mode 100644 index 0000000..6b233c5 --- /dev/null +++ b/integration-test/src/jvmTest/typescript/server/server-common.ts @@ -0,0 +1,402 @@ +import {McpServer} from '@modelcontextprotocol/sdk/server/mcp'; +import {z} from 'zod'; +import { + type CallToolResult, + type GetPromptResult, + type PrimitiveSchemaDefinition, + type ReadResourceResult, + type ResourceLink, +} from '@modelcontextprotocol/sdk/types'; + +export function registerTestUtils(server: McpServer) { + // Register a simple tool that returns a greeting + server.registerTool( + 'greet', + { + title: 'Greeting Tool', + description: 'A simple greeting tool', + inputSchema: { + name: z.string().describe('Name to greet'), + }, + }, + async ({name}): Promise => { + return { + content: [ + { + type: 'text', + text: `Hello, ${name}!`, + }, + ], + }; + } + ); + + // Register a tool that sends multiple greetings with notifications + server.tool( + 'multi-greet', + 'A tool that sends different greetings with delays between them', + { + name: z.string().describe('Name to greet'), + }, + { + title: 'Multiple Greeting Tool', + readOnlyHint: true, + openWorldHint: false + }, + async ({name}, extra): Promise => { + const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + + await server.sendLoggingMessage({ + level: "debug", + data: `Starting multi-greet for ${name}` + }, extra.sessionId); + + await sleep(200); + + await server.sendLoggingMessage({ + level: "info", + data: `Sending first greeting to ${name}` + }, extra.sessionId); + + await sleep(200); + + await server.sendLoggingMessage({ + level: "info", + data: `Sending second greeting to ${name}` + }, extra.sessionId); + + return { + content: [ + { + type: 'text', + text: `Good morning, ${name}!`, + } + ], + }; + } + ); + + // Register a tool that demonstrates elicitation (user input collection) + server.tool( + 'collect-user-info', + 'A tool that collects user information through elicitation', + { + infoType: z.enum(['contact', 'preferences', 'feedback']).describe('Type of information to collect'), + }, + async ({infoType}): Promise => { + let message: string; + let requestedSchema: { + type: 'object'; + properties: Record; + required?: string[]; + }; + + switch (infoType) { + case 'contact': + message = 'Please provide your contact information'; + requestedSchema = { + type: 'object', + properties: { + name: { + type: 'string', + title: 'Full Name', + description: 'Your full name', + }, + email: { + type: 'string', + title: 'Email Address', + description: 'Your email address', + format: 'email', + }, + phone: { + type: 'string', + title: 'Phone Number', + description: 'Your phone number (optional)', + }, + }, + required: ['name', 'email'], + }; + break; + case 'preferences': + message = 'Please set your preferences'; + requestedSchema = { + type: 'object', + properties: { + theme: { + type: 'string', + title: 'Theme', + description: 'Choose your preferred theme', + enum: ['light', 'dark', 'auto'], + enumNames: ['Light', 'Dark', 'Auto'], + }, + notifications: { + type: 'boolean', + title: 'Enable Notifications', + description: 'Would you like to receive notifications?', + default: true, + }, + frequency: { + type: 'string', + title: 'Notification Frequency', + description: 'How often would you like notifications?', + enum: ['daily', 'weekly', 'monthly'], + enumNames: ['Daily', 'Weekly', 'Monthly'], + }, + }, + required: ['theme'], + }; + break; + case 'feedback': + message = 'Please provide your feedback'; + requestedSchema = { + type: 'object', + properties: { + rating: { + type: 'integer', + title: 'Rating', + description: 'Rate your experience (1-5)', + minimum: 1, + maximum: 5, + }, + comments: { + type: 'string', + title: 'Comments', + description: 'Additional comments (optional)', + maxLength: 500, + }, + recommend: { + type: 'boolean', + title: 'Would you recommend this?', + description: 'Would you recommend this to others?', + }, + }, + required: ['rating', 'recommend'], + }; + break; + default: + throw new Error(`Unknown info type: ${infoType}`); + } + + try { + // Use the underlying server instance to elicit input from the client + const result = await server.server.elicitInput({ + message, + requestedSchema, + }); + + if (result.action === 'accept') { + return { + content: [ + { + type: 'text', + text: `Thank you! Collected ${infoType} information: ${JSON.stringify(result.content, null, 2)}`, + }, + ], + }; + } else if (result.action === 'decline') { + return { + content: [ + { + type: 'text', + text: `No information was collected. User declined ${infoType} information request.`, + }, + ], + }; + } else { + return { + content: [ + { + type: 'text', + text: `Information collection was cancelled by the user.`, + }, + ], + }; + } + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error collecting ${infoType} information: ${error}`, + }, + ], + }; + } + } + ); + + // Register a simple prompt + server.registerPrompt( + 'greeting-template', + { + title: 'Greeting Template', + description: 'A simple greeting prompt template', + argsSchema: { + name: z.string().describe('Name to include in greeting'), + }, + }, + async ({name}): Promise => { + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `Please greet ${name} in a friendly manner.`, + }, + }, + ], + }; + } + ); + + // Register a tool specifically for testing resumability + server.tool( + 'start-notification-stream', + 'Starts sending periodic notifications for testing resumability', + { + interval: z.number().describe('Interval in milliseconds between notifications').default(100), + count: z.number().describe('Number of notifications to send (0 for 100)').default(50), + }, + async ({interval, count}, extra): Promise => { + const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + let counter = 0; + + while (count === 0 || counter < count) { + counter++; + try { + await server.sendLoggingMessage({ + level: "info", + data: `Periodic notification #${counter} at ${new Date().toISOString()}` + }, extra.sessionId); + } catch (error) { + console.error("Error sending notification:", error); + } + // Wait for the specified interval + await sleep(interval); + } + + return { + content: [ + { + type: 'text', + text: `Started sending periodic notifications every ${interval}ms`, + } + ], + }; + } + ); + + // Create a simple resource at a fixed URI + server.registerResource( + 'greeting-resource', + 'https://example.com/greetings/default', + { + title: 'Default Greeting', + description: 'A simple greeting resource', + mimeType: 'text/plain' + }, + async (): Promise => { + return { + contents: [ + { + uri: 'https://example.com/greetings/default', + text: 'Hello, world!', + }, + ], + }; + } + ); + + // Create additional resources + server.registerResource( + 'example-file-1', + 'file:///example/file1.txt', + { + title: 'Example File 1', + description: 'First example file for ResourceLink demonstration', + mimeType: 'text/plain' + }, + async (): Promise => { + return { + contents: [ + { + uri: 'file:///example/file1.txt', + text: 'This is the content of file 1', + }, + ], + }; + } + ); + + server.registerResource( + 'example-file-2', + 'file:///example/file2.txt', + { + title: 'Example File 2', + description: 'Second example file for ResourceLink demonstration', + mimeType: 'text/plain' + }, + async (): Promise => { + return { + contents: [ + { + uri: 'file:///example/file2.txt', + text: 'This is the content of file 2', + }, + ], + }; + } + ); + + // Register a tool that returns ResourceLinks + server.registerTool( + 'list-files', + { + title: 'List Files with ResourceLinks', + description: 'Returns a list of files as ResourceLinks without embedding their content', + inputSchema: { + includeDescriptions: z.boolean().optional().describe('Whether to include descriptions in the resource links'), + }, + }, + async ({includeDescriptions = true}): Promise => { + const resourceLinks: ResourceLink[] = [ + { + type: 'resource_link', + uri: 'https://example.com/greetings/default', + name: 'Default Greeting', + mimeType: 'text/plain', + ...(includeDescriptions && {description: 'A simple greeting resource'}) + }, + { + type: 'resource_link', + uri: 'file:///example/file1.txt', + name: 'Example File 1', + mimeType: 'text/plain', + ...(includeDescriptions && {description: 'First example file for ResourceLink demonstration'}) + }, + { + type: 'resource_link', + uri: 'file:///example/file2.txt', + name: 'Example File 2', + mimeType: 'text/plain', + ...(includeDescriptions && {description: 'Second example file for ResourceLink demonstration'}) + } + ]; + + return { + content: [ + { + type: 'text', + text: 'Here are the available files as resource links:', + }, + ...resourceLinks, + { + type: 'text', + text: '\nYou can read any of these resources using their URI.', + } + ], + }; + } + ); +} diff --git a/integration-test/src/jvmTest/typescript/server/stdio-server.ts b/integration-test/src/jvmTest/typescript/server/stdio-server.ts new file mode 100644 index 0000000..0d71eb8 --- /dev/null +++ b/integration-test/src/jvmTest/typescript/server/stdio-server.ts @@ -0,0 +1,20 @@ +import {McpServer} from '@modelcontextprotocol/sdk/server/mcp'; +import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio'; +import {registerTestUtils} from './server-common'; + +async function main() { + const server = new McpServer({ + name: 'simple-stdio-server', + version: '1.0.0', + }, {capabilities: {logging: {}}}); + + registerTestUtils(server); + + const transport = new StdioServerTransport(); + await server.connect(transport); +} + +main().catch((err) => { + console.error('Failed to start stdio server:', err); + process.exit(1); +}); diff --git a/integration-test/src/jvmTest/typescript/tsconfig.json b/integration-test/src/jvmTest/typescript/tsconfig.json new file mode 100644 index 0000000..58e3c67 --- /dev/null +++ b/integration-test/src/jvmTest/typescript/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "strict": true, + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["**/*.ts"] +} diff --git a/knit.properties b/knit.properties new file mode 100644 index 0000000..1c74097 --- /dev/null +++ b/knit.properties @@ -0,0 +1,3 @@ +# Knit configuration +knit.dir=docs/src/main/kotlin/ +knit.package=io.modelcontextprotocol.kotlin.sdk.examples diff --git a/kotlin-sdk-client/Module.md b/kotlin-sdk-client/Module.md index 7154911..4e6fd63 100644 --- a/kotlin-sdk-client/Module.md +++ b/kotlin-sdk-client/Module.md @@ -1,10 +1,78 @@ # Module kotlin-sdk-client -The Model Context Protocol (MCP) Kotlin SDK client module provides a multiplatform implementation for connecting to and -interacting with MCP servers. +`kotlin-sdk-client` is the multiplatform MCP client for Kotlin. It handles the MCP handshake, enforces capabilities, and +gives you typed APIs to call tools, prompts, resources, completions, and logging endpoints on MCP servers. It reuses +protocol types from `kotlin-sdk-core` and ships with transports for CLI and Ktor-based networking. -## Features +## What the module provides -- Cross-platform support for JVM, WASM, JS, and Native -- Built-in support for multiple transport protocols (stdio, SSE, WebSocket) -- Type-safe API for resource management and server interactions +- **Client runtime**: The `Client` class (or `mcpClient` helper) wraps initialization, capability checks, and + request/notification plumbing. After connecting, it exposes `serverCapabilities`, `serverVersion`, and + `serverInstructions`. +- **Typed operations**: Convenience functions like `listTools`, `callTool`, `listResources`, `readResource`, + `listPrompts`, `complete`, `setLoggingLevel`, and subscription helpers for resources. +- **Capability enforcement**: Methods fail fast if the server does not advertise the needed capability; optional + strictness mirrors the protocol options used by the server SDK. +- **Transports**: Ready-to-use implementations for STDIO (CLI interoperability), Ktor SSE (`SseClientTransport` with + POST back-channel), Ktor WebSocket, and Streamable HTTP for environments that prefer request/response streaming. +- **Ktor client integration**: Extension helpers wire MCP over Ktor client engines with minimal setup for SSE, + Streamable HTTP, or WebSocket. + +## Typical client setup + +1. Declare client capabilities with `ClientOptions` (e.g., sampling, roots, elicitation, experimental). +2. Create a transport suitable for your environment. +3. Connect using `mcpClient` or instantiate `Client` and call `connect(transport)`. +4. Use typed APIs to interact with the server; results and errors use the shared MCP types. + +### Minimal STDIO client + +```kotlin +val client = mcpClient( + clientInfo = Implementation(name = "demo-client", version = "1.0.0"), + clientOptions = ClientOptions( + capabilities = ClientCapabilities( + tools = ClientCapabilities.Tools(), + resources = ClientCapabilities.Resources(subscribe = true), + ) + ), + transport = StdioClientTransport( + System.`in`.asSource().buffered(), + System.out.asSink().buffered() + ) +) + +val tools = client.listTools() +val result = client.callTool("hello", mapOf("name" to "Kotlin")) +println(result.content) +``` + +### Ktor-based transports + +- **SSE** (streaming GET + POST back-channel): create `SseClientTransport(httpClient, url)` (or + `HttpClient.mcpSseTransport(url)`); sessions are keyed by `sessionId` managed by the SDK. +- **WebSocket**: use `WebSocketClientTransport(httpClient, url)` (or `HttpClient.mcpWebSocketTransport(url)`) to get + bidirectional messaging in one connection. +- **Streamable HTTP**: `StreamableHttpClientTransport(httpClient, url)` enables MCP over streaming HTTP + (or `HttpClient.mcpStreamableHttpTransport(url)`). + +## Feature usage highlights + +- **Tools**: `callTool` accepts raw maps or `CallToolRequest`; metadata keys are validated per MCP rules. +- **Prompts & completion**: `getPrompt`, `listPrompts`, and `complete` wrap common prompt flows. +- **Resources**: `listResources`, `listResourceTemplates`, `readResource`, and `subscribeResource` cover discovery, + templating, and updates (when the server allows subscriptions). +- **Roots**: If enabled, register local roots with `addRoot`/`addRoots`; the client responds to `RootsList` requests + from the server. +- **Notifications**: Built-in handlers cover initialization, cancellation, progress, and roots list changes; you can + register more handlers via the underlying `Protocol` API. + +## Diagnostics and lifecycle + +- Capability assertions prevent calling endpoints the server does not expose; errors surface with clear messages. +- Logging uses `KotlinLogging`; enable DEBUG to inspect transport and handshake behavior. +- `close()` tears down the transport and cancels in-flight requests. + +Use this module whenever you need an MCP-aware Kotlin client—whether embedding in a CLI over STDIO or talking to remote +servers via Ktor SSE/WebSocket/streamable HTTP transports. Public APIs are explicit so downstream users can rely on +stable, well-typed surface areas across platforms. diff --git a/kotlin-sdk-client/api/kotlin-sdk-client.api b/kotlin-sdk-client/api/kotlin-sdk-client.api index f0782da..d856f5b 100644 --- a/kotlin-sdk-client/api/kotlin-sdk-client.api +++ b/kotlin-sdk-client/api/kotlin-sdk-client.api @@ -1,52 +1,60 @@ public class io/modelcontextprotocol/kotlin/sdk/client/Client : io/modelcontextprotocol/kotlin/sdk/shared/Protocol { - public fun (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/client/ClientOptions;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/client/ClientOptions;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/Implementation;Lio/modelcontextprotocol/kotlin/sdk/client/ClientOptions;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Implementation;Lio/modelcontextprotocol/kotlin/sdk/client/ClientOptions;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun addRoot (Ljava/lang/String;Ljava/lang/String;)V public final fun addRoots (Ljava/util/List;)V protected final fun assertCapability (Ljava/lang/String;Ljava/lang/String;)V - protected fun assertCapabilityForMethod (Lio/modelcontextprotocol/kotlin/sdk/Method;)V - protected fun assertNotificationCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V - public fun assertRequestHandlerCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V - public final fun callTool (Lio/modelcontextprotocol/kotlin/sdk/CallToolRequest;ZLio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun callTool (Ljava/lang/String;Ljava/util/Map;ZLio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun callTool$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/CallToolRequest;ZLio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public static synthetic fun callTool$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Ljava/lang/String;Ljava/util/Map;ZLio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun complete (Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun complete$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + protected fun assertCapabilityForMethod (Lio/modelcontextprotocol/kotlin/sdk/types/Method;)V + protected fun assertNotificationCapability (Lio/modelcontextprotocol/kotlin/sdk/types/Method;)V + public fun assertRequestHandlerCapability (Lio/modelcontextprotocol/kotlin/sdk/types/Method;)V + public final fun callTool (Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun callTool (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun callTool$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun callTool$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun complete (Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun complete$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public fun connect (Lio/modelcontextprotocol/kotlin/sdk/shared/Transport;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun getPrompt (Lio/modelcontextprotocol/kotlin/sdk/GetPromptRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun getPrompt$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/GetPromptRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun getServerCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities; - public final fun getServerVersion ()Lio/modelcontextprotocol/kotlin/sdk/Implementation; - public final fun listPrompts (Lio/modelcontextprotocol/kotlin/sdk/ListPromptsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun listPrompts$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/ListPromptsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun listResourceTemplates (Lio/modelcontextprotocol/kotlin/sdk/ListResourceTemplatesRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun listResourceTemplates$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/ListResourceTemplatesRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun listResources (Lio/modelcontextprotocol/kotlin/sdk/ListResourcesRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun listResources$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/ListResourcesRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun listTools (Lio/modelcontextprotocol/kotlin/sdk/ListToolsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun listTools$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/ListToolsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun getPrompt (Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun getPrompt$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun getServerCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities; + public final fun getServerInstructions ()Ljava/lang/String; + public final fun getServerVersion ()Lio/modelcontextprotocol/kotlin/sdk/types/Implementation; + public final fun listPrompts (Lio/modelcontextprotocol/kotlin/sdk/types/ListPromptsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun listPrompts$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/types/ListPromptsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun listResourceTemplates (Lio/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun listResourceTemplates$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun listResources (Lio/modelcontextprotocol/kotlin/sdk/types/ListResourcesRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun listResources$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/types/ListResourcesRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun listTools (Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun listTools$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final fun ping (Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun ping$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun readResource (Lio/modelcontextprotocol/kotlin/sdk/ReadResourceRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun readResource$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/ReadResourceRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun readResource (Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun readResource$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final fun removeRoot (Ljava/lang/String;)Z public final fun removeRoots (Ljava/util/List;)I public final fun sendRootsListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun setElicitationCompleteHandler (Lkotlin/jvm/functions/Function1;)V public final fun setElicitationHandler (Lkotlin/jvm/functions/Function1;)V - public final fun setLoggingLevel (Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun setLoggingLevel$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun subscribeResource (Lio/modelcontextprotocol/kotlin/sdk/SubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun subscribeResource$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/SubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun unsubscribeResource (Lio/modelcontextprotocol/kotlin/sdk/UnsubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun unsubscribeResource$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/UnsubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun setLoggingLevel (Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun setLoggingLevel$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun subscribeResource (Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun subscribeResource$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun unsubscribeResource (Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun unsubscribeResource$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + protected fun wrapRequestHandler (Lio/modelcontextprotocol/kotlin/sdk/types/Method;Lkotlin/jvm/functions/Function3;)Lkotlin/jvm/functions/Function3; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/ClientKt { + public static final fun mcpClient (Lio/modelcontextprotocol/kotlin/sdk/types/Implementation;Lio/modelcontextprotocol/kotlin/sdk/client/ClientOptions;Lio/modelcontextprotocol/kotlin/sdk/shared/Transport;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun mcpClient$default (Lio/modelcontextprotocol/kotlin/sdk/types/Implementation;Lio/modelcontextprotocol/kotlin/sdk/client/ClientOptions;Lio/modelcontextprotocol/kotlin/sdk/shared/Transport;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; } public final class io/modelcontextprotocol/kotlin/sdk/client/ClientOptions : io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptions { public fun ()V - public fun (Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities;Z)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;ZLkotlin/coroutines/CoroutineContext;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;ZLkotlin/coroutines/CoroutineContext;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; } public final class io/modelcontextprotocol/kotlin/sdk/client/KtorClientKt { @@ -56,32 +64,53 @@ public final class io/modelcontextprotocol/kotlin/sdk/client/KtorClientKt { public static synthetic fun mcpSseTransport-5_5nbZA$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/SseClientTransport; } -public final class io/modelcontextprotocol/kotlin/sdk/client/SseClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { - public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +public final class io/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions { + public synthetic fun (JJDIILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JJDILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getInitialReconnectionDelay-UwyO8pc ()J + public final fun getMaxReconnectionDelay-UwyO8pc ()J + public final fun getMaxRetries ()I + public final fun getReconnectionDelayMultiplier ()D + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } -public final class io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { +public final class io/modelcontextprotocol/kotlin/sdk/client/SseClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransport { + public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransport { + public static final field BUFFER_SIZE J public fun (Lkotlinx/io/Source;Lkotlinx/io/Sink;)V - public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun (Lkotlinx/io/Source;Lkotlinx/io/Sink;Lkotlinx/io/Source;)V + public fun (Lkotlinx/io/Source;Lkotlinx/io/Sink;Lkotlinx/io/Source;Lkotlinx/coroutines/channels/Channel;)V + public fun (Lkotlinx/io/Source;Lkotlinx/io/Sink;Lkotlinx/io/Source;Lkotlinx/coroutines/channels/Channel;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lkotlinx/io/Source;Lkotlinx/io/Sink;Lkotlinx/io/Source;Lkotlinx/coroutines/channels/Channel;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V } -public final class io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { +public final class io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport$StderrSeverity : java/lang/Enum { + public static final field DEBUG Lio/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport$StderrSeverity; + public static final field FATAL Lio/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport$StderrSeverity; + public static final field IGNORE Lio/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport$StderrSeverity; + public static final field INFO Lio/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport$StderrSeverity; + public static final field WARNING Lio/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport$StderrSeverity; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport$StderrSeverity; + public static fun values ()[Lio/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport$StderrSeverity; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransport { + public fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions;ILkotlin/jvm/functions/Function1;)V + public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions;ILkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun getProtocolVersion ()Ljava/lang/String; public final fun getSessionId ()Ljava/lang/String; - public final fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun send$default (Lio/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport;Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun send (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun send$default (Lio/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport;Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final fun setProtocolVersion (Ljava/lang/String;)V - public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun terminateSession (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } @@ -93,8 +122,12 @@ public final class io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpError } public final class io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpMcpKtorClientExtensionsKt { + public static final fun mcpStreamableHttp (Lio/ktor/client/HttpClient;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun mcpStreamableHttp$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static final fun mcpStreamableHttp-BZiP2OM (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun mcpStreamableHttp-BZiP2OM$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun mcpStreamableHttpTransport (Lio/ktor/client/HttpClient;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport; + public static synthetic fun mcpStreamableHttpTransport$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport; public static final fun mcpStreamableHttpTransport-5_5nbZA (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport; public static synthetic fun mcpStreamableHttpTransport-5_5nbZA$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport; } diff --git a/kotlin-sdk-client/build.gradle.kts b/kotlin-sdk-client/build.gradle.kts index 7ba3de2..fbb1300 100644 --- a/kotlin-sdk-client/build.gradle.kts +++ b/kotlin-sdk-client/build.gradle.kts @@ -4,19 +4,19 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl plugins { id("mcp.multiplatform") + id("mcp.detekt") id("mcp.publishing") id("mcp.dokka") alias(libs.plugins.kotlinx.binary.compatibility.validator) + `netty-convention` } kotlin { iosArm64() iosX64() iosSimulatorArm64() - watchosX64() watchosArm64() watchosSimulatorArm64() - tvosX64() tvosArm64() tvosSimulatorArm64() js { @@ -40,8 +40,22 @@ kotlin { commonTest { dependencies { implementation(kotlin("test")) - implementation(libs.ktor.client.mock) + implementation(project(":kotlin-sdk-testing")) + implementation(libs.kotest.assertions.core) implementation(libs.kotlinx.coroutines.test) + implementation(libs.ktor.client.logging) + implementation(libs.ktor.client.mock) + implementation(libs.ktor.server.websockets) + } + } + + jvmTest { + dependencies { + implementation(libs.awaitility) + implementation(libs.ktor.client.apache5) + implementation(libs.mockk) + implementation(libs.junit.jupiter.params) + implementation(dependencies.platform(libs.netty.bom)) runtimeOnly(libs.slf4j.simple) } } diff --git a/kotlin-sdk-client/detekt-baseline-main.xml b/kotlin-sdk-client/detekt-baseline-main.xml new file mode 100644 index 0000000..c34e75c --- /dev/null +++ b/kotlin-sdk-client/detekt-baseline-main.xml @@ -0,0 +1,7 @@ + + + + + LoopWithTooManyJumpStatements:StreamableHttpClientTransport.kt:StreamableHttpClientTransport$while + + diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt index ea63150..3352104 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt @@ -1,59 +1,85 @@ package io.modelcontextprotocol.kotlin.sdk.client import io.github.oshai.kotlinlogging.KotlinLogging -import io.modelcontextprotocol.kotlin.sdk.CallToolRequest -import io.modelcontextprotocol.kotlin.sdk.CallToolResult -import io.modelcontextprotocol.kotlin.sdk.CallToolResultBase -import io.modelcontextprotocol.kotlin.sdk.ClientCapabilities -import io.modelcontextprotocol.kotlin.sdk.CompatibilityCallToolResult -import io.modelcontextprotocol.kotlin.sdk.CompleteRequest -import io.modelcontextprotocol.kotlin.sdk.CompleteResult -import io.modelcontextprotocol.kotlin.sdk.CreateElicitationRequest -import io.modelcontextprotocol.kotlin.sdk.CreateElicitationResult -import io.modelcontextprotocol.kotlin.sdk.EmptyRequestResult -import io.modelcontextprotocol.kotlin.sdk.GetPromptRequest -import io.modelcontextprotocol.kotlin.sdk.GetPromptResult -import io.modelcontextprotocol.kotlin.sdk.Implementation -import io.modelcontextprotocol.kotlin.sdk.InitializeRequest -import io.modelcontextprotocol.kotlin.sdk.InitializeResult -import io.modelcontextprotocol.kotlin.sdk.InitializedNotification -import io.modelcontextprotocol.kotlin.sdk.LATEST_PROTOCOL_VERSION -import io.modelcontextprotocol.kotlin.sdk.ListPromptsRequest -import io.modelcontextprotocol.kotlin.sdk.ListPromptsResult -import io.modelcontextprotocol.kotlin.sdk.ListResourceTemplatesRequest -import io.modelcontextprotocol.kotlin.sdk.ListResourceTemplatesResult -import io.modelcontextprotocol.kotlin.sdk.ListResourcesRequest -import io.modelcontextprotocol.kotlin.sdk.ListResourcesResult -import io.modelcontextprotocol.kotlin.sdk.ListRootsRequest -import io.modelcontextprotocol.kotlin.sdk.ListRootsResult -import io.modelcontextprotocol.kotlin.sdk.ListToolsRequest -import io.modelcontextprotocol.kotlin.sdk.ListToolsResult -import io.modelcontextprotocol.kotlin.sdk.LoggingLevel -import io.modelcontextprotocol.kotlin.sdk.LoggingMessageNotification.SetLevelRequest -import io.modelcontextprotocol.kotlin.sdk.Method -import io.modelcontextprotocol.kotlin.sdk.PingRequest -import io.modelcontextprotocol.kotlin.sdk.ReadResourceRequest -import io.modelcontextprotocol.kotlin.sdk.ReadResourceResult -import io.modelcontextprotocol.kotlin.sdk.Root -import io.modelcontextprotocol.kotlin.sdk.RootsListChangedNotification -import io.modelcontextprotocol.kotlin.sdk.SUPPORTED_PROTOCOL_VERSIONS -import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities -import io.modelcontextprotocol.kotlin.sdk.SubscribeRequest -import io.modelcontextprotocol.kotlin.sdk.UnsubscribeRequest +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi import io.modelcontextprotocol.kotlin.sdk.shared.Protocol import io.modelcontextprotocol.kotlin.sdk.shared.ProtocolOptions +import io.modelcontextprotocol.kotlin.sdk.shared.RequestHandlerExtra import io.modelcontextprotocol.kotlin.sdk.shared.RequestOptions import io.modelcontextprotocol.kotlin.sdk.shared.Transport +import io.modelcontextprotocol.kotlin.sdk.types.BooleanSchema +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.CompleteRequest +import io.modelcontextprotocol.kotlin.sdk.types.CompleteResult +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest +import io.modelcontextprotocol.kotlin.sdk.types.DoubleSchema +import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequest +import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestFormParams +import io.modelcontextprotocol.kotlin.sdk.types.ElicitResult +import io.modelcontextprotocol.kotlin.sdk.types.ElicitationCompleteNotification +import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.InitializeResult +import io.modelcontextprotocol.kotlin.sdk.types.InitializedNotification +import io.modelcontextprotocol.kotlin.sdk.types.IntegerSchema +import io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION +import io.modelcontextprotocol.kotlin.sdk.types.LegacyTitledEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.ListPromptsRequest +import io.modelcontextprotocol.kotlin.sdk.types.ListPromptsResult +import io.modelcontextprotocol.kotlin.sdk.types.ListResourceTemplatesRequest +import io.modelcontextprotocol.kotlin.sdk.types.ListResourceTemplatesResult +import io.modelcontextprotocol.kotlin.sdk.types.ListResourcesRequest +import io.modelcontextprotocol.kotlin.sdk.types.ListResourcesResult +import io.modelcontextprotocol.kotlin.sdk.types.ListRootsRequest +import io.modelcontextprotocol.kotlin.sdk.types.ListRootsResult +import io.modelcontextprotocol.kotlin.sdk.types.ListToolsRequest +import io.modelcontextprotocol.kotlin.sdk.types.ListToolsResult +import io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.PingRequest +import io.modelcontextprotocol.kotlin.sdk.types.PrimitiveSchemaDefinition +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult +import io.modelcontextprotocol.kotlin.sdk.types.Request +import io.modelcontextprotocol.kotlin.sdk.types.RequestMeta +import io.modelcontextprotocol.kotlin.sdk.types.RequestResult +import io.modelcontextprotocol.kotlin.sdk.types.Root +import io.modelcontextprotocol.kotlin.sdk.types.RootsListChangedNotification +import io.modelcontextprotocol.kotlin.sdk.types.SUPPORTED_PROTOCOL_VERSIONS +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.SetLevelRequest +import io.modelcontextprotocol.kotlin.sdk.types.SetLevelRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.StringSchema +import io.modelcontextprotocol.kotlin.sdk.types.SubscribeRequest +import io.modelcontextprotocol.kotlin.sdk.types.TitledMultiSelectEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.TitledSingleSelectEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.UnsubscribeRequest +import io.modelcontextprotocol.kotlin.sdk.types.UntitledMultiSelectEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.UntitledSingleSelectEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.supportsUrl +import io.modelcontextprotocol.kotlin.sdk.types.toJson import kotlinx.atomicfu.atomic import kotlinx.atomicfu.getAndUpdate import kotlinx.atomicfu.update import kotlinx.collections.immutable.minus import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.toPersistentSet +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers +import kotlinx.serialization.SerializationException import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonArray +import kotlin.coroutines.CoroutineContext import kotlin.coroutines.cancellation.CancellationException private val logger = KotlinLogging.logger {} @@ -62,12 +88,41 @@ private val logger = KotlinLogging.logger {} * Options for configuring the MCP client. * * @property capabilities The capabilities this client supports. - * @property enforceStrictCapabilities Whether to strictly enforce capabilities when interacting with the server. + * @param enforceStrictCapabilities Whether to strictly enforce capabilities when interacting with the server. + * @param handlerCoroutineContext Coroutine context for inbound handlers. See [ProtocolOptions.handlerCoroutineContext]. */ public class ClientOptions( public val capabilities: ClientCapabilities = ClientCapabilities(), enforceStrictCapabilities: Boolean = true, -) : ProtocolOptions(enforceStrictCapabilities = enforceStrictCapabilities) + handlerCoroutineContext: CoroutineContext = Dispatchers.Default, +) : ProtocolOptions( + enforceStrictCapabilities = enforceStrictCapabilities, + handlerCoroutineContext = handlerCoroutineContext, +) + +/** + * Initializes and connects an MCP client using the provided clientInfo [Implementation], client options, + * and transport mechanism. + * + * @param clientInfo The implementation details of the MCP client, including its name, version, and other metadata. + * @param clientOptions Optional client configuration settings, such as supported capabilities + * and strict enforcement options. Defaults to a new instance of [ClientOptions]. + * @param transport The transport mechanism used for communication. + * @return An instance of [Client] that is connected and ready for use with the specified transport. + */ +@ExperimentalMcpApi +public suspend fun mcpClient( + clientInfo: Implementation, + clientOptions: ClientOptions = ClientOptions(), + transport: Transport, +): Client { + val client = Client( + clientInfo = clientInfo, + options = clientOptions, + ) + client.connect(transport) + return client +} /** * An MCP client on top of a pluggable transport. @@ -91,6 +146,14 @@ public open class Client(private val clientInfo: Implementation, options: Client public var serverCapabilities: ServerCapabilities? = null private set + /** + * Optional human-readable instructions or description from the server. + * + * @return Instructions provided by the server, or `null` if none were given or initialization is not yet complete. + */ + public var serverInstructions: String? = null + private set + /** * Retrieves the server's reported version information after initialization. * @@ -121,11 +184,12 @@ public open class Client(private val clientInfo: Implementation, options: Client "prompts" -> caps?.prompts != null "resources" -> caps?.resources != null "tools" -> caps?.tools != null + "tasks" -> caps?.tasks != null else -> true } - if (!hasCapability) { - throw IllegalStateException("Server does not support $capability (required for $method)") + check(hasCapability) { + "Server does not support $capability (required for $method)" } } @@ -140,46 +204,61 @@ public open class Client(private val clientInfo: Implementation, options: Client try { val message = InitializeRequest( - protocolVersion = LATEST_PROTOCOL_VERSION, - capabilities = capabilities, - clientInfo = clientInfo, + InitializeRequestParams( + protocolVersion = LATEST_PROTOCOL_VERSION, + capabilities = capabilities, + clientInfo = clientInfo, + ), ) val result = request(message) if (!SUPPORTED_PROTOCOL_VERSIONS.contains(result.protocolVersion)) { - throw IllegalStateException( + error( "Server's protocol version is not supported: ${result.protocolVersion}", ) } serverCapabilities = result.capabilities serverVersion = result.serverInfo + serverInstructions = result.instructions notification(InitializedNotification()) + enableConcurrentDispatch() } catch (error: Throwable) { + logger.error(error) { "Failed to initialize client: ${error.message}" } close() - if (error !is CancellationException) { - throw IllegalStateException("Error connecting to transport: ${error.message}") - } - throw error + when (error) { + is CancellationException, + is McpException, + is StreamableHttpError, + is SerializationException, + -> throw error + + else -> throw IllegalStateException("Error connecting to transport: ${error.message}", error) + } } } override fun assertCapabilityForMethod(method: Method) { when (method) { Method.Defined.LoggingSetLevel -> { - if (serverCapabilities?.logging == null) { - throw IllegalStateException("Server does not support logging (required for $method)") + checkNotNull(serverCapabilities?.logging) { + "Server does not support logging (required for $method)" } } Method.Defined.PromptsGet, Method.Defined.PromptsList, - Method.Defined.CompletionComplete, -> { - if (serverCapabilities?.prompts == null) { - throw IllegalStateException("Server does not support prompts (required for $method)") + checkNotNull(serverCapabilities?.prompts) { + "Server does not support prompts (required for $method)" + } + } + + Method.Defined.CompletionComplete -> { + checkNotNull(serverCapabilities?.completions) { + "Server does not support completions (required for $method)" } } @@ -192,24 +271,26 @@ public open class Client(private val clientInfo: Implementation, options: Client val resCaps = serverCapabilities?.resources ?: error("Server does not support resources (required for $method)") - if (method == Method.Defined.ResourcesSubscribe && resCaps.subscribe != true) { - throw IllegalStateException( - "Server does not support resource subscriptions (required for $method)", - ) + if (method == Method.Defined.ResourcesSubscribe) { + check(resCaps.subscribe == true) { + "Server does not support resource subscriptions (required for $method)" + } } } - Method.Defined.ToolsCall, - Method.Defined.ToolsList, - -> { - if (serverCapabilities?.tools == null) { - throw IllegalStateException("Server does not support tools (required for $method)") + Method.Defined.ToolsCall, Method.Defined.ToolsList -> { + checkNotNull(serverCapabilities?.tools) { + "Server does not support tools (required for $method)" } } - Method.Defined.Initialize, - Method.Defined.Ping, - -> { + Method.Defined.TasksGet, + Method.Defined.TasksResult, + Method.Defined.TasksList, + Method.Defined.TasksCancel, + -> assertTasksCapabilityForMethod(method) + + Method.Defined.Initialize, Method.Defined.Ping -> { // No specific capability required } @@ -219,13 +300,35 @@ public open class Client(private val clientInfo: Implementation, options: Client } } + private fun assertTasksCapabilityForMethod(method: Method) { + val tasks = serverCapabilities?.tasks + ?: error("Server does not support tasks (required for $method)") + when (method) { + Method.Defined.TasksList -> checkNotNull(tasks.list) { + "Server does not support listing tasks (required for $method)" + } + + Method.Defined.TasksCancel -> checkNotNull(tasks.cancel) { + "Server does not support cancelling tasks (required for $method)" + } + + else -> { + // TasksGet, TasksResult: base tasks capability suffices. + } + } + } + override fun assertNotificationCapability(method: Method) { when (method) { Method.Defined.NotificationsRootsListChanged -> { - if (capabilities.roots?.listChanged != true) { - throw IllegalStateException( - "Client does not support roots list changed notifications (required for $method)", - ) + check(capabilities.roots?.listChanged == true) { + "Client does not support roots list changed notifications (required for $method)" + } + } + + Method.Defined.NotificationsTasksStatus -> { + checkNotNull(capabilities.tasks) { + "Client does not support tasks (required for $method)" } } @@ -245,26 +348,20 @@ public open class Client(private val clientInfo: Implementation, options: Client override fun assertRequestHandlerCapability(method: Method) { when (method) { Method.Defined.SamplingCreateMessage -> { - if (capabilities.sampling == null) { - throw IllegalStateException( - "Client does not support sampling capability (required for $method)", - ) + checkNotNull(capabilities.sampling) { + "Client does not support sampling capability (required for $method)" } } Method.Defined.RootsList -> { - if (capabilities.roots == null) { - throw IllegalStateException( - "Client does not support roots capability (required for $method)", - ) + checkNotNull(capabilities.roots) { + "Client does not support roots capability (required for $method)" } } Method.Defined.ElicitationCreate -> { - if (capabilities.elicitation == null) { - throw IllegalStateException( - "Client does not support elicitation capability (required for $method)", - ) + checkNotNull(capabilities.elicitation) { + "Client does not support elicitation capability (required for $method)" } } @@ -276,14 +373,33 @@ public open class Client(private val clientInfo: Implementation, options: Client } } + /** + * Wraps incoming-request handlers with SEP-1577 client-side enforcement. + * + * For `sampling/createMessage`: if the incoming request carries `tools` or + * `toolChoice` but this client did not advertise [ClientCapabilities.Sampling.tools], + * the wrapper throws an [McpException] with JSON-RPC error code `InvalidParams` + * before the user-supplied handler runs. Matches the TypeScript SDK wrapper in + * `Client.setRequestHandler`. + */ + override fun wrapRequestHandler( + method: Method, + block: suspend (T, RequestHandlerExtra) -> RequestResult?, + ): suspend (T, RequestHandlerExtra) -> RequestResult? { + if (method != Method.Defined.SamplingCreateMessage) return block + return { request, extra -> + (request as? CreateMessageRequest)?.let { validateSamplingToolsCapability(it, capabilities) } + block(request, extra) + } + } + /** * Sends a ping request to the server to check connectivity. * * @param options Optional request options. * @throws IllegalStateException If the server does not support the ping method (unlikely). */ - public suspend fun ping(options: RequestOptions? = null): EmptyRequestResult = - request(PingRequest(), options) + public suspend fun ping(options: RequestOptions? = null): EmptyResult = request(PingRequest(), options) /** * Sends a completion request to the server, typically to generate or complete some content. @@ -291,7 +407,7 @@ public open class Client(private val clientInfo: Implementation, options: Client * @param params The completion request parameters. * @param options Optional request options. * @return The completion result returned by the server, or `null` if none. - * @throws IllegalStateException If the server does not support prompts or completion. + * @throws IllegalStateException If the server does not support completions. */ public suspend fun complete(params: CompleteRequest, options: RequestOptions? = null): CompleteResult = request(params, options) @@ -303,8 +419,8 @@ public open class Client(private val clientInfo: Implementation, options: Client * @param options Optional request options. * @throws IllegalStateException If the server does not support logging. */ - public suspend fun setLoggingLevel(level: LoggingLevel, options: RequestOptions? = null): EmptyRequestResult = - request(SetLevelRequest(level), options) + public suspend fun setLoggingLevel(level: LoggingLevel, options: RequestOptions? = null): EmptyResult = + request(SetLevelRequest(SetLevelRequestParams(level)), options) /** * Retrieves a prompt by name from the server. @@ -376,10 +492,8 @@ public open class Client(private val clientInfo: Implementation, options: Client * @param options Optional request options. * @throws IllegalStateException If the server does not support resource subscriptions. */ - public suspend fun subscribeResource( - request: SubscribeRequest, - options: RequestOptions? = null, - ): EmptyRequestResult = request(request, options) + public suspend fun subscribeResource(request: SubscribeRequest, options: RequestOptions? = null): EmptyResult = + request(request, options) /** * Unsubscribes from resource changes on the server. @@ -388,17 +502,18 @@ public open class Client(private val clientInfo: Implementation, options: Client * @param options Optional request options. * @throws IllegalStateException If the server does not support resource subscriptions. */ - public suspend fun unsubscribeResource( - request: UnsubscribeRequest, - options: RequestOptions? = null, - ): EmptyRequestResult = request(request, options) + public suspend fun unsubscribeResource(request: UnsubscribeRequest, options: RequestOptions? = null): EmptyResult = + request(request, options) /** - * Calls a tool on the server by name, passing the specified arguments. + * Calls a tool on the server by name, passing the specified arguments and metadata. * * @param name The name of the tool to call. * @param arguments A map of argument names to values for the tool. - * @param compatibility Whether to use compatibility mode for older protocol versions. + * @param meta A map of metadata key-value pairs. Keys must follow MCP specification format. + * - Optional prefix: dot-separated labels followed by slash (e.g., "api.example.com/") + * - Name: alphanumeric start/end, may contain hyphens, underscores, dots, alphanumerics + * - Reserved prefixes starting with "mcp" or "modelcontextprotocol" are forbidden * @param options Optional request options. * @return The result of the tool call, or `null` if none. * @throws IllegalStateException If the server does not support tools. @@ -406,45 +521,34 @@ public open class Client(private val clientInfo: Implementation, options: Client public suspend fun callTool( name: String, arguments: Map, - compatibility: Boolean = false, + meta: Map = emptyMap(), options: RequestOptions? = null, - ): CallToolResultBase? { - val jsonArguments = arguments.mapValues { (_, value) -> - when (value) { - is String -> JsonPrimitive(value) - is Number -> JsonPrimitive(value) - is Boolean -> JsonPrimitive(value) - is JsonElement -> value - null -> JsonNull - else -> JsonPrimitive(value.toString()) - } - } + ): CallToolResult { + validateMetaKeys(meta.keys) + + val jsonArguments = arguments.toJson() + val jsonMeta = meta.toJson() val request = CallToolRequest( - name = name, - arguments = JsonObject(jsonArguments), + CallToolRequestParams( + name = name, + arguments = JsonObject(jsonArguments), + meta = RequestMeta(JsonObject(jsonMeta)), + ), ) - return callTool(request, compatibility, options) + return callTool(request, options) } /** * Calls a tool on the server using a [CallToolRequest] object. * * @param request The request object containing the tool name and arguments. - * @param compatibility Whether to use compatibility mode for older protocol versions. * @param options Optional request options. * @return The result of the tool call, or `null` if none. * @throws IllegalStateException If the server does not support tools. */ - public suspend fun callTool( - request: CallToolRequest, - compatibility: Boolean = false, - options: RequestOptions? = null, - ): CallToolResultBase? = if (compatibility) { - request(request, options) - } else { - request(request, options) - } + public suspend fun callTool(request: CallToolRequest, options: RequestOptions? = null): CallToolResult = + request(request, options) /** * Lists all available tools on the server. @@ -467,9 +571,9 @@ public open class Client(private val clientInfo: Implementation, options: Client * @throws IllegalStateException If the client does not support roots. */ public fun addRoot(uri: String, name: String) { - if (capabilities.roots == null) { + checkNotNull(capabilities.roots) { logger.error { "Failed to add root '$name': Client does not support roots capability" } - throw IllegalStateException("Client does not support roots capability.") + "Client does not support roots capability." } logger.info { "Adding root: $name ($uri)" } roots.update { current -> current.put(uri, Root(uri, name)) } @@ -482,9 +586,9 @@ public open class Client(private val clientInfo: Implementation, options: Client * @throws IllegalStateException If the client does not support roots. */ public fun addRoots(rootsToAdd: List) { - if (capabilities.roots == null) { + checkNotNull(capabilities.roots) { logger.error { "Failed to add roots: Client does not support roots capability" } - throw IllegalStateException("Client does not support roots capability.") + "Client does not support roots capability." } logger.info { "Adding ${rootsToAdd.size} roots" } roots.update { current -> current.putAll(rootsToAdd.associateBy { it.uri }) } @@ -498,9 +602,8 @@ public open class Client(private val clientInfo: Implementation, options: Client * @throws IllegalStateException If the client does not support roots. */ public fun removeRoot(uri: String): Boolean { - if (capabilities.roots == null) { - logger.error { "Failed to remove root '$uri': Client does not support roots capability" } - throw IllegalStateException("Client does not support roots capability.") + checkNotNull(capabilities.roots) { + "Client does not support roots capability." } logger.info { "Removing root: $uri" } val oldMap = roots.getAndUpdate { current -> current.remove(uri) } @@ -523,9 +626,9 @@ public open class Client(private val clientInfo: Implementation, options: Client * @throws IllegalStateException If the client does not support roots. */ public fun removeRoots(uris: List): Int { - if (capabilities.roots == null) { + checkNotNull(capabilities.roots) { logger.error { "Failed to remove roots: Client does not support roots capability" } - throw IllegalStateException("Client does not support roots capability.") + "Client does not support roots capability." } logger.info { "Removing ${uris.size} roots" } @@ -556,25 +659,165 @@ public open class Client(private val clientInfo: Implementation, options: Client /** * Sets the elicitation handler. * + * The handler receives both form-mode ([ElicitRequestFormParams]) and URL-mode + * ([io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestURLParams]) requests; + * branch on `request.params` to tell them apart. For URL mode, + * the host application must obtain explicit user consent and display the target domain before + * navigating — the SDK never opens or validates the URL — and should return + * [ElicitResult.Action.Decline] or [ElicitResult.Action.Cancel] when it cannot or will not proceed. + * A URL-mode [ElicitResult.Action.Accept] only signals consent; the outcome arrives out-of-band via + * [setElicitationCompleteHandler]. + * + * When a form-mode handler returns [ElicitResult.Action.Accept], any properties missing from + * [ElicitResult.content] are automatically populated with default values defined in the + * elicitation schema. URL-mode responses carry no content. + * * @param handler The elicitation handler. * @throws IllegalStateException if the client does not support elicitation. */ - public fun setElicitationHandler(handler: (CreateElicitationRequest) -> CreateElicitationResult) { - if (capabilities.elicitation == null) { + public fun setElicitationHandler(handler: (ElicitRequest) -> ElicitResult) { + checkNotNull(capabilities.elicitation) { logger.error { "Failed to set elicitation handler: Client does not support elicitation" } - throw IllegalStateException("Client does not support elicitation.") + "Client does not support elicitation." } logger.info { "Setting the elicitation handler" } - setRequestHandler(Method.Defined.ElicitationCreate) { request, _ -> - handler(request) + setRequestHandler(Method.Defined.ElicitationCreate) { request, _ -> + val result = handler(request) + applyElicitationDefaults(request, result) + } + } + + /** + * Sets the handler invoked when the server reports that a URL-mode elicitation has completed. + * + * The handler is called for every `notifications/elicitation/complete` notification. Because the + * server only sends this for an out-of-band (URL-mode) interaction, the client must support url-mode + * elicitation. The client is responsible for correlating the notification's `elicitationId` with a + * pending elicitation, ignoring unknown or already-completed identifiers, and providing a manual way + * to continue if a notification never arrives. + * + * @param handler Invoked with each completion notification. + * @throws IllegalStateException if the client does not support url-mode elicitation. + */ + public fun setElicitationCompleteHandler(handler: (ElicitationCompleteNotification) -> Unit) { + check(capabilities.elicitation.supportsUrl) { + logger.error { + "Failed to set elicitation-complete handler: client does not support url-mode elicitation" + } + "Client does not support url-mode elicitation." + } + logger.info { "Setting the elicitation-complete handler" } + + setNotificationHandler( + Method.Defined.NotificationsElicitationComplete, + ) { notification -> + handler(notification) + CompletableDeferred(Unit) } } // --- Internal Handlers --- - private suspend fun handleListRoots(): ListRootsResult { + private fun applyElicitationDefaults(request: ElicitRequest, result: ElicitResult): ElicitResult { + if (result.action != ElicitResult.Action.Accept) return result + val formParams = request.params as? ElicitRequestFormParams ?: return result + val content = result.content ?: return result + + val merged = buildMap { + putAll(content) + for ((key, schemaDef) in formParams.requestedSchema.properties) { + if (key !in content) { + schemaDef.defaultJsonValue()?.let { put(key, it) } + } + } + } + + return if (merged.size == content.size) { + result + } else { + result.copy(content = JsonObject(merged)) + } + } + + @Suppress("DEPRECATION", "CyclomaticComplexMethod") + private fun PrimitiveSchemaDefinition.defaultJsonValue(): JsonElement? = when (this) { + is StringSchema -> default?.let { JsonPrimitive(it) } + + is IntegerSchema -> default?.let { JsonPrimitive(it) } + + is DoubleSchema -> default?.let { JsonPrimitive(it) } + + is BooleanSchema -> default?.let { JsonPrimitive(it) } + + is UntitledSingleSelectEnumSchema -> default?.let { JsonPrimitive(it) } + + is TitledSingleSelectEnumSchema -> default?.let { JsonPrimitive(it) } + + is LegacyTitledEnumSchema -> default?.let { JsonPrimitive(it) } + + is UntitledMultiSelectEnumSchema -> default?.let { list -> + buildJsonArray { list.forEach { add(JsonPrimitive(it)) } } + } + + is TitledMultiSelectEnumSchema -> default?.let { list -> + buildJsonArray { list.forEach { add(JsonPrimitive(it)) } } + } + } + + private fun handleListRoots(): ListRootsResult { val rootList = roots.value.values.toList() return ListRootsResult(rootList) } + + /** + * Validates meta keys according to MCP specification. + * + * Key format: [prefix/]name + * - Prefix (optional): dot-separated labels + slash + * - Reserved prefixes contain "modelcontextprotocol" or "mcp" as complete labels + * - Name: alphanumeric start/end, may contain hyphens, underscores, dots (empty allowed) + */ + private fun validateMetaKeys(keys: Set) { + val labelPattern = Regex("[a-zA-Z]([a-zA-Z0-9-]*[a-zA-Z0-9])?") + val namePattern = Regex("[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?") + + keys.forEach { key -> + require(key.isNotEmpty()) { "Meta key cannot be empty" } + + val (prefix, name) = key.split('/', limit = 2).let { parts -> + when (parts.size) { + 1 -> null to parts[0] + 2 -> parts[0] to parts[1] + else -> throw IllegalArgumentException("Unexpected split result for key: $key") + } + } + + // Validate prefix if present + prefix?.let { + require(it.isNotEmpty()) { "Invalid _meta key '$key': prefix cannot be empty" } + + val labels = it.split('.') + require(labels.all { label -> label.matches(labelPattern) }) { + "Invalid _meta key '$key': prefix labels must start with a letter, end with letter/digit, " + + "and contain only letters, digits, or hyphens" + } + + require( + labels.none { label -> + label.equals("modelcontextprotocol", ignoreCase = true) || + label.equals("mcp", ignoreCase = true) + }, + ) { + "Invalid _meta key '$key': prefix cannot contain reserved labels 'modelcontextprotocol' or 'mcp'" + } + } + + // Validate name (empty allowed) + require(name.isEmpty() || name.matches(namePattern)) { + "Invalid _meta key '$key': name must start and end with alphanumeric characters, " + + "and contain only alphanumerics, hyphens, underscores, or dots" + } + } + } } diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/KtorClient.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/KtorClient.kt index ccc1496..0c69504 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/KtorClient.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/KtorClient.kt @@ -2,9 +2,9 @@ package io.modelcontextprotocol.kotlin.sdk.client import io.ktor.client.HttpClient import io.ktor.client.request.HttpRequestBuilder -import io.modelcontextprotocol.kotlin.sdk.Implementation import io.modelcontextprotocol.kotlin.sdk.LIB_VERSION import io.modelcontextprotocol.kotlin.sdk.shared.IMPLEMENTATION_NAME +import io.modelcontextprotocol.kotlin.sdk.types.Implementation import kotlin.time.Duration /** @@ -13,7 +13,7 @@ import kotlin.time.Duration * @param urlString Optional URL of the MCP server. * @param reconnectionTime Optional duration to wait before attempting to reconnect. * @param requestBuilder Optional lambda to configure the HTTP request. - * @return A [SSEClientTransport] configured for MCP communication. + * @return A [SseClientTransport] configured for MCP communication. */ public fun HttpClient.mcpSseTransport( urlString: String? = null, diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions.kt new file mode 100644 index 0000000..95c5bde --- /dev/null +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions.kt @@ -0,0 +1,44 @@ +package io.modelcontextprotocol.kotlin.sdk.client + +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +/** + * Options for controlling SSE reconnection behavior. + * + * @property initialReconnectionDelay The initial delay before the first reconnection attempt. + * @property maxReconnectionDelay The maximum delay between reconnection attempts. + * @property reconnectionDelayMultiplier The factor by which the delay grows on each attempt. + * @property maxRetries The maximum number of reconnection attempts per disconnect. + */ +public class ReconnectionOptions( + public val initialReconnectionDelay: Duration = 1.seconds, + public val maxReconnectionDelay: Duration = 30.seconds, + public val reconnectionDelayMultiplier: Double = 1.5, + public val maxRetries: Int = 2, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as ReconnectionOptions + + if (reconnectionDelayMultiplier != other.reconnectionDelayMultiplier) return false + if (maxRetries != other.maxRetries) return false + if (initialReconnectionDelay != other.initialReconnectionDelay) return false + if (maxReconnectionDelay != other.maxReconnectionDelay) return false + + return true + } + + override fun hashCode(): Int { + var result = reconnectionDelayMultiplier.hashCode() + result = 31 * result + maxRetries + result = 31 * result + initialReconnectionDelay.hashCode() + result = 31 * result + maxReconnectionDelay.hashCode() + return result + } + + override fun toString(): String = + "ReconnectionOptions(initialReconnectionDelay=$initialReconnectionDelay, maxReconnectionDelay=$maxReconnectionDelay, reconnectionDelayMultiplier=$reconnectionDelayMultiplier, maxRetries=$maxRetries)" +} diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SamplingValidation.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SamplingValidation.kt new file mode 100644 index 0000000..1290cb0 --- /dev/null +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SamplingValidation.kt @@ -0,0 +1,24 @@ +package io.modelcontextprotocol.kotlin.sdk.client + +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.RPCError + +/** + * Validates SEP-1577 client-side enforcement: an incoming `sampling/createMessage` + * request that carries `tools` or `toolChoice` requires the client to have advertised + * the `sampling.tools` sub-capability. When the capability is missing, throws an + * [McpException] with JSON-RPC error code `InvalidParams`, matching TypeScript SDK + * client enforcement. + */ +internal fun validateSamplingToolsCapability(request: CreateMessageRequest, capabilities: ClientCapabilities) { + val params = request.params + if (params.tools == null && params.toolChoice == null) return + if (capabilities.sampling?.tools != null) return + val field = if (params.tools != null) "tools" else "toolChoice" + throw McpException( + code = RPCError.ErrorCode.INVALID_PARAMS, + message = "Client does not support sampling with tools but request contains $field parameter", + ) +} diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SSEClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseClientTransport.kt similarity index 54% rename from kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SSEClientTransport.kt rename to kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseClientTransport.kt index 950f37f..0295eb9 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SSEClientTransport.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseClientTransport.kt @@ -1,5 +1,7 @@ package io.modelcontextprotocol.kotlin.sdk.client +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.client.HttpClient import io.ktor.client.plugins.sse.ClientSSESession import io.ktor.client.plugins.sse.sseSession @@ -9,32 +11,29 @@ import io.ktor.client.request.setBody import io.ktor.client.statement.bodyAsText import io.ktor.http.ContentType import io.ktor.http.HttpHeaders -import io.ktor.http.Url import io.ktor.http.append import io.ktor.http.isSuccess import io.ktor.http.protocolWithAuthority -import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage -import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport -import io.modelcontextprotocol.kotlin.sdk.shared.McpJson +import io.modelcontextprotocol.kotlin.sdk.shared.AbstractClientTransport +import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.McpJson import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel -import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.ensureActive import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.serialization.SerializationException -import kotlin.concurrent.atomics.AtomicBoolean import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlin.time.Duration -@Deprecated("Use SseClientTransport instead", ReplaceWith("SseClientTransport"), DeprecationLevel.WARNING) -public typealias SSEClientTransport = SseClientTransport - /** * Client transport for SSE: this will connect to a server using Server-Sent Events for receiving * messages and make separate POST requests for sending messages. @@ -45,81 +44,68 @@ public class SseClientTransport( private val urlString: String?, private val reconnectionTime: Duration? = null, private val requestBuilder: HttpRequestBuilder.() -> Unit = {}, -) : AbstractTransport() { - private val initialized: AtomicBoolean = AtomicBoolean(false) +) : AbstractClientTransport() { + + override val logger: KLogger = KotlinLogging.logger {} + private val endpoint = CompletableDeferred() private lateinit var session: ClientSSESession private lateinit var scope: CoroutineScope private var job: Job? = null + private val origin: String by lazy { + session.call.request.url.protocolWithAuthority + } + private val baseUrl: String by lazy { session.call.request.url.let { url -> val path = url.encodedPath when { - path.isEmpty() -> url.protocolWithAuthority - path.endsWith("/") -> url.protocolWithAuthority + path.removeSuffix("/") - else -> url.protocolWithAuthority + path.take(path.lastIndexOf("/")) + path.isEmpty() -> origin + path.endsWith("/") -> origin + path.removeSuffix("/") + else -> origin + path.take(path.lastIndexOf("/")) } } } - override suspend fun start() { - check(initialized.compareAndSet(expectedValue = false, newValue = true)) { - "SSEClientTransport already started! If using Client class, note that connect() calls start() automatically." - } - - try { - session = urlString?.let { - client.sseSession( - urlString = it, - reconnectionTime = reconnectionTime, - block = requestBuilder, - ) - } ?: client.sseSession( + override suspend fun initialize() { + session = urlString?.let { + client.sseSession( + urlString = it, reconnectionTime = reconnectionTime, block = requestBuilder, ) - scope = CoroutineScope(session.coroutineContext + SupervisorJob()) + } ?: client.sseSession( + reconnectionTime = reconnectionTime, + block = requestBuilder, + ) + scope = CoroutineScope(session.coroutineContext + SupervisorJob()) - job = scope.launch(CoroutineName("SseMcpClientTransport.connect#${hashCode()}")) { - collectMessages() - } - - endpoint.await() - } catch (e: Exception) { - closeResources() - initialized.store(false) - throw e + job = scope.launch(CoroutineName("SseMcpClientTransport.connect#${hashCode()}")) { + collectMessages() } + + endpoint.await() } @OptIn(ExperimentalCoroutinesApi::class) - override suspend fun send(message: JSONRPCMessage) { - check(initialized.load()) { "SseClientTransport is not initialized!" } + override suspend fun performSend(message: JSONRPCMessage, options: TransportSendOptions?) { check(job?.isActive == true) { "SseClientTransport is closed!" } check(endpoint.isCompleted) { "Not connected!" } - try { - val response = client.post(endpoint.getCompleted()) { - requestBuilder() - headers.append(HttpHeaders.ContentType, ContentType.Application.Json) - setBody(McpJson.encodeToString(message)) - } - - if (!response.status.isSuccess()) { - val text = response.bodyAsText() - error("Error POSTing to endpoint (HTTP ${response.status}): $text") - } - } catch (e: Throwable) { - _onError(e) - throw e + val response = client.post(endpoint.getCompleted()) { + requestBuilder() + headers.append(HttpHeaders.ContentType, ContentType.Application.Json) + setBody(McpJson.encodeToString(message)) } - } - override suspend fun close() { - check(initialized.load()) { "SseClientTransport is not initialized!" } - closeResources() + if (!response.status.isSuccess()) { + val text = response.bodyAsText() + error("Error POSTing to endpoint (HTTP ${response.status}): $text") + } + + logger.debug { "Client successfully sent message via SSE $endpoint" } } private suspend fun CoroutineScope.collectMessages() { @@ -150,14 +136,26 @@ public class SseClientTransport( throw e } finally { closeResources() + invokeOnCloseCallback() } } + /** + * Resolves and completes [endpoint] based on [eventData]. + * Uses full URLs as-is, treats absolute paths as origin-relative, + * and relative paths as relative to [baseUrl]. + */ private fun handleEndpoint(eventData: String) { try { - val path = if (eventData.startsWith("/")) eventData.substring(1) else eventData - val endpointUrl = Url("$baseUrl/$path") - endpoint.complete(endpointUrl.toString()) + val endpointUrl = if (eventData.startsWith("http://") || eventData.startsWith("https://")) { + eventData + } else if (eventData.startsWith("/")) { + origin + eventData + } else { + "$baseUrl/$eventData" + } + endpoint.complete(endpointUrl) + logger.debug { "Client connected to endpoint: $endpointUrl" } } catch (e: Throwable) { _onError(e) endpoint.completeExceptionally(e) @@ -174,18 +172,16 @@ public class SseClientTransport( } } - private suspend fun closeResources() { - if (!initialized.compareAndSet(expectedValue = true, newValue = false)) return - - job?.cancelAndJoin() - try { - if (::session.isInitialized) session.cancel() - if (::scope.isInitialized) scope.cancel() - endpoint.cancel() - } catch (e: Throwable) { - _onError(e) + override suspend fun closeResources() { + withContext(NonCancellable) { + job?.cancel() + try { + if (::session.isInitialized) session.cancel() + if (::scope.isInitialized) scope.cancel() + endpoint.cancel() + } catch (e: Throwable) { + _onError(e) + } } - - _onClose() } } diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt index 583cec6..5e990ee 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt @@ -1,134 +1,349 @@ package io.modelcontextprotocol.kotlin.sdk.client +import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KotlinLogging -import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport.StderrSeverity.DEBUG +import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport.StderrSeverity.FATAL +import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport.StderrSeverity.IGNORE +import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport.StderrSeverity.INFO +import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport.StderrSeverity.WARNING import io.modelcontextprotocol.kotlin.sdk.internal.IODispatcher -import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport +import io.modelcontextprotocol.kotlin.sdk.shared.AbstractClientTransport import io.modelcontextprotocol.kotlin.sdk.shared.ReadBuffer +import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions import io.modelcontextprotocol.kotlin.sdk.shared.serializeMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.CONNECTION_CLOSED +import io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.INTERNAL_ERROR +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ClosedSendChannelException +import kotlinx.coroutines.channels.ProducerScope import kotlinx.coroutines.channels.consumeEach +import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.coroutines.yield import kotlinx.io.Buffer +import kotlinx.io.IOException import kotlinx.io.Sink import kotlinx.io.Source import kotlinx.io.buffered import kotlinx.io.readByteArray import kotlinx.io.writeString -import kotlin.concurrent.atomics.AtomicBoolean +import kotlinx.serialization.SerializationException import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlin.coroutines.CoroutineContext +import kotlin.jvm.JvmOverloads /** - * A transport implementation for JSON-RPC communication that leverages standard input and output streams. + * A transport implementation for JSON-RPC communication over standard I/O streams. * - * This class reads from an input stream to process incoming JSON-RPC messages and writes JSON-RPC messages - * to an output stream. + * Reads JSON-RPC messages from [input] and writes messages to [output]. Optionally monitors + * [error] stream for stderr output with configurable severity handling. + * + * ## Usage Example + * ```kotlin + * val process = ProcessBuilder("mcp-server").start() + * + * val transport = StdioClientTransport( + * input = process.inputStream.asSource().buffered(), + * output = process.outputStream.asSink().buffered(), + * error = process.errorStream.asSource().buffered() + * ) { stderrLine -> + * when { + * stderrLine.contains("error", ignoreCase = true) -> StderrSeverity.FATAL + * stderrLine.contains("warning", ignoreCase = true) -> StderrSeverity.WARNING + * else -> StderrSeverity.INFO + * } + * } + * + * transport.start() + * ``` * * @param input The input stream where messages are received. * @param output The output stream where messages are sent. + * @param error Optional error stream for stderr monitoring. + * @param sendChannel Channel for outbound messages. Default: buffered channel + * (implementation-default capacity). + * @param classifyStderr Callback to classify stderr lines. Return [StderrSeverity.FATAL] to fail transport, + * or [StderrSeverity.WARNING] / [StderrSeverity.INFO] / [StderrSeverity.DEBUG] + * to log, or [StderrSeverity.IGNORE] to discard. + * Default value: [StderrSeverity.DEBUG]. + * @see MCP Specification + * @see [Channel.BUFFERED] */ @OptIn(ExperimentalAtomicApi::class) -public class StdioClientTransport(private val input: Source, private val output: Sink) : AbstractTransport() { - private val logger = KotlinLogging.logger {} - private val ioCoroutineContext: CoroutineContext = IODispatcher - private val scope by lazy { - CoroutineScope(ioCoroutineContext + SupervisorJob()) +public class StdioClientTransport @JvmOverloads public constructor( + private val input: Source, + private val output: Sink, + private val error: Source? = null, + private val sendChannel: Channel = Channel(Channel.BUFFERED), + private val classifyStderr: (String) -> StderrSeverity = { DEBUG }, +) : AbstractClientTransport() { + + override val logger: KLogger = KotlinLogging.logger {} + + private companion object { + /** + * Buffer size for I/O operations. + * 8KB is optimal for most systems (matches default page size). + */ + const val BUFFER_SIZE = 8 * 1024L } - private var job: Job? = null - private val initialized: AtomicBoolean = AtomicBoolean(false) - private val sendChannel = Channel(Channel.UNLIMITED) - private val readBuffer = ReadBuffer() - override suspend fun start() { - if (!initialized.compareAndSet(expectedValue = false, newValue = true)) { - error("StdioClientTransport already started!") - } + /** + * Severity classification for stderr messages. + * + * - [FATAL]: Calls error handler and terminates transport. + * - [WARNING]: Logs at WARN level, transport continues. + * - [INFO]: Logs at INFO level, transport continues. + * - [DEBUG]: Logs at DEBUG level, transport continues. + * - [IGNORE]: Discards message silently, transport continues. + */ + public enum class StderrSeverity { FATAL, WARNING, INFO, DEBUG, IGNORE } + private val ioCoroutineContext: CoroutineContext = IODispatcher + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + + override suspend fun initialize() { logger.debug { "Starting StdioClientTransport..." } - val outputStream = output.buffered() + // Producers run on IODispatcher for I/O + // Collector runs on Default dispatcher for message handling + scope.launch(CoroutineName("StdioClientTransport.IO#${hashCode()}")) { + var writeJob: Job? = null + val mainScope = this + try { + // Explicitly use ioCoroutineContext for I/O operations + writeJob = launch(ioCoroutineContext) { + logger.debug { "Write coroutine started." } + output.buffered().use { sink -> + sendChannel.consumeEach { message -> + sendOutboundMessage(message, sink, mainScope) + yield() // Giving other coroutines a chance to run + } + } + } - job = scope.launch(CoroutineName("StdioClientTransport.IO#${hashCode()}")) { - val readJob = launch { - logger.debug { "Read coroutine started." } - try { - input.use { - while (isActive) { - val buffer = Buffer() - val bytesRead = input.readAtMostTo(buffer, 8192) - if (bytesRead == -1L) break - if (bytesRead > 0L) { - readBuffer.append(buffer.readByteArray()) - processReadBuffer() + val eventsFlow = channelFlow { + launch(ioCoroutineContext) { + logger.debug { "Read stdin coroutine started." } + val readBuffer = ReadBuffer() // parses bytes into JSONRPCMessage + readSource(stream = ProcessStream.Stdin, source = input, channel = this@channelFlow) { bytes -> + readBuffer.append(bytes) + do { + val msg = readBuffer.readMessage() + msg?.let { send(Event.JsonRpc(msg)) } + } while (msg != null) + } + }.invokeOnCompletion { + logger.debug(it) { "Read stdin coroutine finished." } + } + + error?.let { source -> + launch(ioCoroutineContext) { + logger.debug { "Read stderr coroutine started." } + readSource( + stream = ProcessStream.Stderr, + source = source, + channel = this@channelFlow, + ) { bytes -> + val str = bytes.decodeToString() + send(Event.StderrEvent(str)) } } } - } catch (e: Exception) { - _onError.invoke(e) - logger.error(e) { "Error reading from input stream" } + } + + // Collect events on handlerCoroutineContext (Dispatchers.Default from parent scope) + // No flowOn necessary - collection runs in parent launch context + eventsFlow + .collect { event -> + when (event) { + is Event.JsonRpc -> { + handleJSONRPCMessage(event.message) + } + + is Event.StderrEvent -> { + val errorSeverity = classifyStderr(event.message) + when (errorSeverity) { + FATAL -> { + runCatching { + _onError( + McpException(INTERNAL_ERROR, "Message in StdErr: ${event.message}"), + ) + } + stopProcessing("Fatal STDERR message received") + } + + WARNING -> { + logger.warn { "STDERR message received: ${event.message}" } + } + + INFO -> { + logger.info { "STDERR message received: ${event.message}" } + } + + DEBUG -> { + logger.debug { "STDERR message received: ${event.message}" } + } + + IGNORE -> { + // do nothing + } + } + } + + is Event.EOFEvent -> { + if (event.stream == ProcessStream.Stdin) { + stopProcessing("EOF in ${event.stream}") + } + } + + is Event.IOErrorEvent -> { + runCatching { _onError(event.cause) } + stopProcessing("IO Error", event.cause) + } + } + } + } finally { + // Wait for write job to complete before closing, matching old implementation + writeJob?.cancelAndJoin() + logger.debug { "Transport coroutine completed, calling onClose" } + invokeOnCloseCallback() + } + } + } + + override suspend fun performSend(message: JSONRPCMessage, options: TransportSendOptions?) { + try { + sendChannel.send(message) + } catch (e: CancellationException) { + throw e // MUST rethrow immediately - don't log, don't wrap + } catch (e: ClosedSendChannelException) { + logger.debug(e) { "Cannot send message: transport is closed" } + throw McpException( + code = CONNECTION_CLOSED, + message = "Transport is closed", + cause = e, + ) + } + } + + override suspend fun closeResources() { + withContext(NonCancellable) { + scope.stopProcessing("Closed") + closeReadSources() + scope.coroutineContext[Job]?.join() // Wait for all coroutines to complete + } + } + + private fun closeReadSources() { + runCatching { input.close() } + .onFailure { logger.debug(it) { "Error closing stdin source" } } + error?.let { source -> + runCatching { source.close() } + .onFailure { logger.debug(it) { "Error closing stderr source" } } + } + } + + private fun sendOutboundMessage(message: JSONRPCMessage, sink: Sink, mainScope: CoroutineScope) { + try { + val json = serializeMessage(message) + sink.writeString(json) + sink.flush() + } catch (e: SerializationException) { + logger.warn(e) { "Can't serialize message" } + runCatching { _onError(McpException(INTERNAL_ERROR, "Serialization error")) } + mainScope.stopProcessing("Can't serialize message", e) + } catch (e: IOException) { + logger.warn(e) { "Can't send message" } + runCatching { _onError(McpException(CONNECTION_CLOSED, "Can't send message. Connection closed")) } + mainScope.stopProcessing("Write I/O failed", e) + } + } + + private suspend fun handleJSONRPCMessage(msg: JSONRPCMessage) { + try { + _onMessage.invoke(msg) + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + logger.error(e) { "Error processing message." } + runCatching { _onError.invoke(e) } + } + } + + private fun CoroutineScope.stopProcessing(reason: String, cause: Throwable? = null) { + sendChannel.close() // Stop accepting new messages + invokeOnCloseCallback() + cancel(reason, cause) // cancel current coroutine context + } + + private suspend fun CoroutineScope.readSource( + stream: ProcessStream, + source: Source, + channel: ProducerScope, + bytesConsumer: suspend (ByteArray) -> Unit, + ) { + val buffer = Buffer() + try { + source.use { + while (isActive) { + val bytesRead = source.readAtMostTo(buffer, BUFFER_SIZE) + if (bytesRead == -1L) { + logger.debug { "EOF reached in $stream" } + channel.send(Event.EOFEvent(stream)) + break + } + + if (bytesRead > 0L) { + val bytes = buffer.readByteArray() + buffer.clear() + bytesConsumer.invoke(bytes) + } + + yield() // Giving other coroutines a chance to run } } - - val writeJob = launch { - logger.debug { "Write coroutine started." } - try { - sendChannel.consumeEach { message -> - val json = serializeMessage(message) - outputStream.writeString(json) - outputStream.flush() - } - } catch (e: Throwable) { - if (isActive) { - _onError.invoke(e) - logger.error(e) { "Error writing to output stream" } - } - } finally { - output.close() - } - } - - readJob.join() - writeJob.cancelAndJoin() - _onClose.invoke() + } catch (exception: IOException) { + logger.debug(exception) { "IOException while reading stream" } + channel.send(Event.IOErrorEvent(stream, exception)) + } finally { + buffer.clear() } } - override suspend fun send(message: JSONRPCMessage) { - if (!initialized.load()) { - error("Transport not started") - } + private enum class ProcessStream { Stdin, Stderr, Stdout } - sendChannel.send(message) - } + /** + * Represents an event in the communication process. + * + * Events are a sealed hierarchy of different types of communication signals + * between processes. These events are used to manage and interpret information + * or errors generated during the operation of the associated transport. + */ + private sealed interface Event { - override suspend fun close() { - if (!initialized.compareAndSet(expectedValue = true, newValue = false)) { - error("Transport is already closed") - } - job?.cancelAndJoin() - input.close() - output.close() - readBuffer.clear() - sendChannel.close() - _onClose.invoke() - } + data class JsonRpc(val message: JSONRPCMessage) : Event - private suspend fun processReadBuffer() { - while (true) { - val msg = readBuffer.readMessage() ?: break - try { - _onMessage.invoke(msg) - } catch (e: Throwable) { - _onError.invoke(e) - logger.error(e) { "Error processing message." } - } + data class StderrEvent(val message: String) : Event + + data class EOFEvent(val stream: ProcessStream) : Event + + data class IOErrorEvent(val stream: ProcessStream, val cause: Throwable) : Event { + override fun toString(): String = "IOErrorEvent(stream=$stream, cause=${cause.message})" } } } diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt index 50f3a51..6df352a 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt @@ -1,5 +1,6 @@ package io.modelcontextprotocol.kotlin.sdk.client +import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.client.HttpClient import io.ktor.client.plugins.sse.ClientSSESession @@ -20,14 +21,17 @@ import io.ktor.http.HttpMethod import io.ktor.http.HttpStatusCode import io.ktor.http.contentType import io.ktor.http.isSuccess +import io.ktor.utils.io.charsets.TooLongLineException import io.ktor.utils.io.readUTF8Line -import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage -import io.modelcontextprotocol.kotlin.sdk.JSONRPCNotification -import io.modelcontextprotocol.kotlin.sdk.JSONRPCRequest -import io.modelcontextprotocol.kotlin.sdk.JSONRPCResponse -import io.modelcontextprotocol.kotlin.sdk.RequestId -import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport -import io.modelcontextprotocol.kotlin.sdk.shared.McpJson +import io.modelcontextprotocol.kotlin.sdk.shared.AbstractClientTransport +import io.modelcontextprotocol.kotlin.sdk.shared.TooLongFrameException +import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse +import io.modelcontextprotocol.kotlin.sdk.types.McpJson +import io.modelcontextprotocol.kotlin.sdk.types.RequestId import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope @@ -36,79 +40,133 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import kotlin.concurrent.atomics.AtomicBoolean -import kotlin.concurrent.atomics.ExperimentalAtomicApi +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi +import kotlin.math.pow import kotlin.time.Duration - -private val logger = KotlinLogging.logger {} +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds private const val MCP_SESSION_ID_HEADER = "mcp-session-id" private const val MCP_PROTOCOL_VERSION_HEADER = "mcp-protocol-version" private const val MCP_RESUMPTION_TOKEN_HEADER = "Last-Event-ID" +private const val MCP_METHOD_HEADER = "Mcp-Method" +private const val MCP_NAME_HEADER = "Mcp-Name" +private const val MCP_BASE64_PREFIX = "=?base64?" +private const val MCP_BASE64_SUFFIX = "?=" /** - * Error class for Streamable HTTP transport errors. + * Default maximum size, in characters, of a single inline SSE event assembled from a POST response. + * + * Mirrors the stdio transport's 16 MiB frame cap: a server that streams `data:` lines without ever + * terminating the event cannot grow the client's buffer without bound. + */ +private const val DEFAULT_MAX_INLINE_SSE_EVENT_SIZE: Int = 16 * 1024 * 1024 + +/** + * Represents an error from the Streamable HTTP transport. + * + * @property code HTTP status code associated with the error, or `null` if unavailable + * @param message detailed error description appended to the exception message */ public class StreamableHttpError(public val code: Int? = null, message: String? = null) : Exception("Streamable HTTP error: $message") +private sealed interface ConnectResult { + data class Success(val session: ClientSSESession) : ConnectResult + data object NonRetryable : ConnectResult + data object Failed : ConnectResult +} + /** - * Client transport for Streamable HTTP: this implements the MCP Streamable HTTP transport specification. - * It will connect to a server using HTTP POST for sending messages and HTTP GET with Server-Sent Events - * for receiving messages. + * Client transport implementing the MCP Streamable HTTP transport specification. + * + * Sends messages via HTTP POST and receives messages via HTTP GET with Server-Sent Events. + * Supports automatic SSE reconnection with exponential backoff, stream resumption via the + * `Last-Event-ID` header, and explicit session termination. + * + * @param client Ktor HTTP client used for all requests + * @param url MCP endpoint URL + * @param reconnectionOptions reconnection backoff and retry-limit settings for the SSE stream + * @param maxInlineSseEventSize maximum size, in characters, of a single inline SSE event parsed from a + * POST response; a server that exceeds it (including by never terminating an event) fails the send + * with [io.modelcontextprotocol.kotlin.sdk.shared.TooLongFrameException]. Defaults to 16 MiB. + * @param requestBuilder builder applied to every outgoing HTTP request, e.g. for adding auth headers */ -@OptIn(ExperimentalAtomicApi::class) public class StreamableHttpClientTransport( private val client: HttpClient, private val url: String, - private val reconnectionTime: Duration? = null, + private val reconnectionOptions: ReconnectionOptions = ReconnectionOptions(), + private val maxInlineSseEventSize: Int = DEFAULT_MAX_INLINE_SSE_EVENT_SIZE, private val requestBuilder: HttpRequestBuilder.() -> Unit = {}, -) : AbstractTransport() { +) : AbstractClientTransport() { + init { + require(maxInlineSseEventSize > 0) { "maxInlineSseEventSize must be greater than 0" } + } + + @Deprecated( + "Use constructor with ReconnectionOptions", + replaceWith = ReplaceWith( + "StreamableHttpClientTransport(client, url, " + + "ReconnectionOptions(initialReconnectionDelay = reconnectionTime ?: 1.seconds), requestBuilder)", + "kotlin.time.Duration.Companion.seconds", + "io.modelcontextprotocol.kotlin.sdk.client.ReconnectionOptions", + ), + ) + public constructor( + client: HttpClient, + url: String, + reconnectionTime: Duration?, + requestBuilder: HttpRequestBuilder.() -> Unit = {}, + ) : this( + client, + url, + ReconnectionOptions(initialReconnectionDelay = reconnectionTime ?: 1.seconds), + requestBuilder = requestBuilder, + ) + + override val logger: KLogger = KotlinLogging.logger {} + + /** Session identifier assigned by the server after initialization, or `null` before connection. */ public var sessionId: String? = null private set + + /** MCP protocol version negotiated with the server, or `null` before connection. */ public var protocolVersion: String? = null - private val initialized: AtomicBoolean = AtomicBoolean(false) - - private var sseSession: ClientSSESession? = null private var sseJob: Job? = null private val scope by lazy { CoroutineScope(SupervisorJob() + Dispatchers.Default) } - private var lastEventId: String? = null + /** Result of an SSE stream collection. Reconnect when [hasPrimingEvent] is true and [receivedResponse] is false. */ + private data class SseStreamResult( + val hasPrimingEvent: Boolean, + val receivedResponse: Boolean, + val lastEventId: String? = null, + val serverRetryDelay: Duration? = null, + ) - override suspend fun start() { - if (!initialized.compareAndSet(expectedValue = false, newValue = true)) { - error("StreamableHttpClientTransport already started!") - } - logger.debug { "Client transport starting..." } + override suspend fun initialize() { + logger.debug { "Client transport is starting..." } } /** * Sends a single message with optional resumption support */ - override suspend fun send(message: JSONRPCMessage) { - send(message, null) - } - - /** - * Sends one or more messages with optional resumption support. - * This is the main send method that matches the TypeScript implementation. - */ - public suspend fun send( - message: JSONRPCMessage, - resumptionToken: String?, - onResumptionToken: ((String) -> Unit)? = null, - ) { + override suspend fun performSend(message: JSONRPCMessage, options: TransportSendOptions?) { logger.debug { "Client sending message via POST to $url: ${McpJson.encodeToString(message)}" } // If we have a resumption token, reconnect the SSE stream with it - resumptionToken?.let { token -> + options?.resumptionToken?.let { token -> startSseSession( resumptionToken = token, - onResumptionToken = onResumptionToken, + onResumptionToken = options.onResumptionToken, replayMessageId = if (message is JSONRPCRequest) message.id else null, ) return @@ -117,6 +175,7 @@ public class StreamableHttpClientTransport( val jsonBody = McpJson.encodeToString(message) val response = client.post(url) { applyCommonHeaders(this) + applyStandardPostHeaders(this, message) headers.append(HttpHeaders.Accept, "${ContentType.Application.Json}, ${ContentType.Text.EventStream}") contentType(ContentType.Application.Json) setBody(jsonBody) @@ -127,7 +186,7 @@ public class StreamableHttpClientTransport( if (response.status == HttpStatusCode.Accepted) { if (message is JSONRPCNotification && message.method == "notifications/initialized") { - startSseSession(onResumptionToken = onResumptionToken) + startSseSession(onResumptionToken = options?.onResumptionToken) } return } @@ -148,41 +207,51 @@ public class StreamableHttpClientTransport( } } - ContentType.Text.EventStream -> handleInlineSse( - response, - onResumptionToken = onResumptionToken, - replayMessageId = if (message is JSONRPCRequest) message.id else null, - ) + ContentType.Text.EventStream -> { + val replayMessageId = if (message is JSONRPCRequest) message.id else null + val result = handleInlineSse(response, replayMessageId, options?.onResumptionToken) + if (result.hasPrimingEvent && !result.receivedResponse) { + startSseSession( + resumptionToken = result.lastEventId, + replayMessageId = replayMessageId, + onResumptionToken = options?.onResumptionToken, + initialServerRetryDelay = result.serverRetryDelay, + ) + } + } else -> { val body = response.bodyAsText() if (response.contentType() == null && body.isBlank()) return val ct = response.contentType()?.toString() ?: "" - val error = StreamableHttpError(-1, "Unexpected content type: $$ct") + val error = StreamableHttpError(-1, "Unexpected content type: $ct") _onError(error) throw error } } } - override suspend fun close() { - if (!initialized.load()) return // Already closed or never started + /** + * Sends one or more messages with optional resumption support. + * This is the main send method that matches the TypeScript implementation. + */ + public suspend fun send( + message: JSONRPCMessage, + resumptionToken: String?, + onResumptionToken: ((String) -> Unit)? = null, + ): Unit = send( + message = message, + options = TransportSendOptions( + resumptionToken = resumptionToken, + onResumptionToken = onResumptionToken, + ), + ) + + override suspend fun closeResources() { logger.debug { "Client transport closing." } - - try { - // Try to terminate session if we have one - terminateSession() - - sseSession?.cancel() - sseJob?.cancelAndJoin() - scope.cancel() - } catch (_: Exception) { - // Ignore errors during cleanup - } finally { - initialized.store(false) - _onClose() - } + sseJob?.cancelAndJoin() + scope.cancel() } /** @@ -208,42 +277,118 @@ public class StreamableHttpClientTransport( } sessionId = null - lastEventId = null logger.debug { "Session terminated successfully" } } - private suspend fun startSseSession( + private fun startSseSession( resumptionToken: String? = null, replayMessageId: RequestId? = null, onResumptionToken: ((String) -> Unit)? = null, + initialServerRetryDelay: Duration? = null, ) { - sseSession?.cancel() - sseJob?.cancelAndJoin() + // Cancel-and-replace: cancel() signals the previous job, join() inside + // the new coroutine ensures it completes before we start collecting. + // This is intentionally non-suspend to avoid blocking performSend. + val previousJob = sseJob + previousJob?.cancel() + sseJob = scope.launch(CoroutineName("StreamableHttpTransport.collect#${hashCode()}")) { + previousJob?.join() + var lastEventId = resumptionToken + var serverRetryDelay = initialServerRetryDelay + var attempt = 0 + var needsDelay = initialServerRetryDelay != null + while (isActive) { + // Delay before (re)connection: skip only for first fresh SSE connection + if (needsDelay) { + delay(getNextReconnectionDelay(attempt, serverRetryDelay)) + } + needsDelay = true + + // Connect + val session = when (val cr = connectSse(lastEventId)) { + is ConnectResult.Success -> { + attempt = 0 + cr.session + } + + ConnectResult.NonRetryable -> return@launch + + ConnectResult.Failed -> { + // Give up after maxRetries consecutive failed connection attempts + if (++attempt >= reconnectionOptions.maxRetries) { + _onError(StreamableHttpError(null, "Maximum reconnection attempts exceeded")) + return@launch + } + continue + } + } + + // Collect + val result = collectSse(session, replayMessageId, onResumptionToken) + lastEventId = result.lastEventId ?: lastEventId + serverRetryDelay = result.serverRetryDelay ?: serverRetryDelay + if (result.receivedResponse) break + } + } + } + + private suspend fun connectSse(lastEventId: String?): ConnectResult { logger.debug { "Client attempting to start SSE session at url: $url" } - try { - sseSession = client.sseSession( - urlString = url, - reconnectionTime = reconnectionTime, - ) { + return try { + val session = client.sseSession(urlString = url, showRetryEvents = true) { method = HttpMethod.Get applyCommonHeaders(this) - accept(ContentType.Text.EventStream) - (resumptionToken ?: lastEventId)?.let { headers.append(MCP_RESUMPTION_TOKEN_HEADER, it) } + accept(ContentType.Application.Json) + lastEventId?.let { headers.append(MCP_RESUMPTION_TOKEN_HEADER, it) } requestBuilder() } logger.debug { "Client SSE session started successfully." } - } catch (e: SSEClientException) { - if (e.response?.status == HttpStatusCode.MethodNotAllowed) { - logger.info { "Server returned 405 for GET/SSE, stream disabled." } - return - } - _onError(e) + ConnectResult.Success(session) + } catch (e: CancellationException) { throw e + } catch (e: SSEClientException) { + if (isNonRetryableSseError(e)) { + ConnectResult.NonRetryable + } else { + logger.debug { "SSE connection failed: ${e.message}" } + ConnectResult.Failed + } + } catch (e: Exception) { + logger.debug { "SSE connection failed: ${e.message}" } + ConnectResult.Failed } + } - sseJob = scope.launch(CoroutineName("StreamableHttpTransport.collect#${hashCode()}")) { - sseSession?.let { collectSse(it, replayMessageId, onResumptionToken) } + private fun getNextReconnectionDelay(attempt: Int, serverRetryDelay: Duration?): Duration { + // Per SSE specification, the server-sent `retry` field sets the reconnection time + // for all subsequent attempts, taking priority over exponential backoff. + serverRetryDelay?.let { return it } + val delay = reconnectionOptions.initialReconnectionDelay * + reconnectionOptions.reconnectionDelayMultiplier.pow(attempt) + return delay.coerceAtMost(reconnectionOptions.maxReconnectionDelay) + } + + /** + * Checks if an SSE session error is non-retryable (404, 405, JSON-only). + * Returns `true` if non-retryable (should stop trying), `false` otherwise. + */ + private fun isNonRetryableSseError(e: SSEClientException): Boolean { + val responseStatus = e.response?.status + val responseContentType = e.response?.contentType() + + return when { + responseStatus == HttpStatusCode.NotFound || responseStatus == HttpStatusCode.MethodNotAllowed -> { + logger.info { "Server returned ${responseStatus.value} for GET/SSE, stream disabled." } + true + } + + responseContentType?.match(ContentType.Application.Json) == true -> { + logger.info { "Server returned application/json for GET/SSE, using JSON-only mode." } + true + } + + else -> false } } @@ -254,15 +399,51 @@ public class StreamableHttpClientTransport( } } + private fun applyStandardPostHeaders(builder: HttpRequestBuilder, message: JSONRPCMessage) { + val (method, params) = when (message) { + is JSONRPCRequest -> message.method to message.params + is JSONRPCNotification -> message.method to message.params + else -> return + } + + builder.headers { + append(MCP_METHOD_HEADER, method) + + val paramsObject = params as? JsonObject ?: return@headers + val mcpName = paramsObject.stringValue("name") ?: paramsObject.stringValue("uri") + mcpName?.let { append(MCP_NAME_HEADER, it.encodeMcpHeaderValue()) } + } + } + + private fun JsonObject.stringValue(key: String): String? = + (get(key) as? JsonPrimitive)?.takeIf { it.isString }?.content + + @OptIn(ExperimentalEncodingApi::class) + private fun String.encodeMcpHeaderValue(): String { + val containsUnsafeCharacters = any { it != '\t' && it.code !in 0x20..0x7e } + val hasEdgeWhitespace = firstOrNull()?.isWhitespace() == true || lastOrNull()?.isWhitespace() == true + val matchesBase64Sentinel = startsWith(MCP_BASE64_PREFIX) && endsWith(MCP_BASE64_SUFFIX) + + if (!containsUnsafeCharacters && !hasEdgeWhitespace && !matchesBase64Sentinel) return this + + return "$MCP_BASE64_PREFIX${Base64.Default.encode(encodeToByteArray())}$MCP_BASE64_SUFFIX" + } + private suspend fun collectSse( session: ClientSSESession, replayMessageId: RequestId?, onResumptionToken: ((String) -> Unit)?, - ) { + ): SseStreamResult { + var hasPrimingEvent = false + var receivedResponse = false + var localLastEventId: String? = null + var localServerRetryDelay: Duration? = null try { session.incoming.collect { event -> + event.retry?.let { localServerRetryDelay = it.milliseconds } event.id?.let { - lastEventId = it + localLastEventId = it + hasPrimingEvent = true onResumptionToken?.invoke(it) } logger.trace { "Client received SSE event: event=${event.event}, data=${event.data}, id=${event.id}" } @@ -271,6 +452,7 @@ public class StreamableHttpClientTransport( event.data?.takeIf { it.isNotEmpty() }?.let { json -> runCatching { McpJson.decodeFromString(json) } .onSuccess { msg -> + if (msg is JSONRPCResponse) receivedResponse = true if (replayMessageId != null && msg is JSONRPCResponse) { _onMessage(msg.copy(id = replayMessageId)) } else { @@ -288,28 +470,38 @@ public class StreamableHttpClientTransport( } catch (t: Throwable) { _onError(t) } + return SseStreamResult(hasPrimingEvent, receivedResponse, localLastEventId, localServerRetryDelay) } private suspend fun handleInlineSse( response: HttpResponse, replayMessageId: RequestId?, onResumptionToken: ((String) -> Unit)?, - ) { + ): SseStreamResult { logger.trace { "Handling inline SSE from POST response" } val channel = response.bodyAsChannel() + var hasPrimingEvent = false + var receivedResponse = false + var localLastEventId: String? = null + var localServerRetryDelay: Duration? = null val sb = StringBuilder() var id: String? = null var eventName: String? = null - suspend fun dispatch(data: String) { + suspend fun dispatch(id: String?, eventName: String?, data: String) { id?.let { - lastEventId = it + localLastEventId = it + hasPrimingEvent = true onResumptionToken?.invoke(it) } + if (data.isBlank()) { + return + } if (eventName == null || eventName == "message") { runCatching { McpJson.decodeFromString(data) } .onSuccess { msg -> + if (msg is JSONRPCResponse) receivedResponse = true if (replayMessageId != null && msg is JSONRPCResponse) { _onMessage(msg.copy(id = replayMessageId)) } else { @@ -321,23 +513,47 @@ public class StreamableHttpClientTransport( throw it } } - // reset - id = null - eventName = null - sb.clear() + if (eventName == "error") { + _onError(StreamableHttpError(null, data)) + return + } } while (!channel.isClosedForRead) { - val line = channel.readUTF8Line() ?: break + // Bound each line so a server that streams a line without ever terminating it cannot + // exhaust client memory; readUTF8Line returns null at the end of the stream. + val line = try { + channel.readUTF8Line(maxInlineSseEventSize) + } catch (_: TooLongLineException) { + throw TooLongFrameException(maxInlineSseEventSize.toLong() + 1, maxInlineSseEventSize) + } + if (line == null) break if (line.isEmpty()) { - dispatch(sb.toString()) + dispatch(id = id, eventName = eventName, data = sb.toString()) + // reset + id = null + eventName = null + sb.clear() continue } when { line.startsWith("id:") -> id = line.substringAfter("id:").trim() + line.startsWith("event:") -> eventName = line.substringAfter("event:").trim() - line.startsWith("data:") -> sb.append(line.substringAfter("data:").trim()) + + line.startsWith("data:") -> { + sb.append(line.substringAfter("data:").trim()) + // Cap an event assembled from many data: lines that never sees a terminating blank line. + if (sb.length > maxInlineSseEventSize) { + throw TooLongFrameException(sb.length.toLong(), maxInlineSseEventSize) + } + } + + line.startsWith("retry:") -> line.substringAfter("retry:").trim().toLongOrNull()?.let { + localServerRetryDelay = it.milliseconds + } } } + return SseStreamResult(hasPrimingEvent, receivedResponse, localLastEventId, localServerRetryDelay) } } diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpMcpKtorClientExtensions.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpMcpKtorClientExtensions.kt index 1a600a3..a618f18 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpMcpKtorClientExtensions.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpMcpKtorClientExtensions.kt @@ -2,10 +2,26 @@ package io.modelcontextprotocol.kotlin.sdk.client import io.ktor.client.HttpClient import io.ktor.client.request.HttpRequestBuilder -import io.modelcontextprotocol.kotlin.sdk.Implementation import io.modelcontextprotocol.kotlin.sdk.LIB_VERSION import io.modelcontextprotocol.kotlin.sdk.shared.IMPLEMENTATION_NAME +import io.modelcontextprotocol.kotlin.sdk.types.Implementation import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +/** + * Returns a new Streamable HTTP transport for the Model Context Protocol using the provided HttpClient. + * + * @param url URL of the MCP server. + * @param reconnectionOptions Options for controlling SSE reconnection behavior. + * @param requestBuilder Optional lambda to configure the HTTP request. + * @return A [StreamableHttpClientTransport] configured for MCP communication. + */ +public fun HttpClient.mcpStreamableHttpTransport( + url: String, + reconnectionOptions: ReconnectionOptions = ReconnectionOptions(), + requestBuilder: HttpRequestBuilder.() -> Unit = {}, +): StreamableHttpClientTransport = + StreamableHttpClientTransport(this, url, reconnectionOptions, requestBuilder = requestBuilder) /** * Returns a new Streamable HTTP transport for the Model Context Protocol using the provided HttpClient. @@ -15,12 +31,42 @@ import kotlin.time.Duration * @param requestBuilder Optional lambda to configure the HTTP request. * @return A [StreamableHttpClientTransport] configured for MCP communication. */ +@Deprecated( + "Use overload with ReconnectionOptions", + replaceWith = ReplaceWith( + "mcpStreamableHttpTransport(url, " + + "ReconnectionOptions(initialReconnectionDelay = reconnectionTime ?: 1.seconds), requestBuilder)", + ), +) public fun HttpClient.mcpStreamableHttpTransport( url: String, - reconnectionTime: Duration? = null, + reconnectionTime: Duration?, requestBuilder: HttpRequestBuilder.() -> Unit = {}, -): StreamableHttpClientTransport = - StreamableHttpClientTransport(this, url, reconnectionTime, requestBuilder = requestBuilder) +): StreamableHttpClientTransport = StreamableHttpClientTransport( + this, + url, + ReconnectionOptions(initialReconnectionDelay = reconnectionTime ?: 1.seconds), + requestBuilder = requestBuilder, +) + +/** + * Creates and connects an MCP client over Streamable HTTP using the provided HttpClient. + * + * @param url URL of the MCP server. + * @param reconnectionOptions Options for controlling SSE reconnection behavior. + * @param requestBuilder Optional lambda to configure the HTTP request. + * @return A connected [Client] ready for MCP communication. + */ +public suspend fun HttpClient.mcpStreamableHttp( + url: String, + reconnectionOptions: ReconnectionOptions = ReconnectionOptions(), + requestBuilder: HttpRequestBuilder.() -> Unit = {}, +): Client { + val transport = mcpStreamableHttpTransport(url, reconnectionOptions, requestBuilder) + val client = Client(Implementation(name = IMPLEMENTATION_NAME, version = LIB_VERSION)) + client.connect(transport) + return client +} /** * Creates and connects an MCP client over Streamable HTTP using the provided HttpClient. @@ -30,12 +76,23 @@ public fun HttpClient.mcpStreamableHttpTransport( * @param requestBuilder Optional lambda to configure the HTTP request. * @return A connected [Client] ready for MCP communication. */ +@Deprecated( + "Use overload with ReconnectionOptions", + replaceWith = ReplaceWith( + "mcpStreamableHttp(url, " + + "ReconnectionOptions(initialReconnectionDelay = reconnectionTime ?: 1.seconds), requestBuilder)", + ), +) public suspend fun HttpClient.mcpStreamableHttp( url: String, - reconnectionTime: Duration? = null, + reconnectionTime: Duration?, requestBuilder: HttpRequestBuilder.() -> Unit = {}, ): Client { - val transport = mcpStreamableHttpTransport(url, reconnectionTime, requestBuilder) + val transport = mcpStreamableHttpTransport( + url, + ReconnectionOptions(initialReconnectionDelay = reconnectionTime ?: 1.seconds), + requestBuilder, + ) val client = Client(Implementation(name = IMPLEMENTATION_NAME, version = LIB_VERSION)) client.connect(transport) return client diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport.kt index 4571907..c02d68e 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport.kt @@ -1,5 +1,6 @@ package io.modelcontextprotocol.kotlin.sdk.client +import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.client.HttpClient import io.ktor.client.plugins.websocket.webSocketSession import io.ktor.client.request.HttpRequestBuilder @@ -10,6 +11,8 @@ import io.modelcontextprotocol.kotlin.sdk.shared.MCP_SUBPROTOCOL import io.modelcontextprotocol.kotlin.sdk.shared.WebSocketMcpTransport import kotlin.properties.Delegates +private val logger = KotlinLogging.logger {} + /** * Client transport for WebSocket: this will connect to a server over the WebSocket protocol. */ @@ -21,8 +24,10 @@ public class WebSocketClientTransport( override var session: WebSocketSession by Delegates.notNull() override suspend fun initializeSession() { - session = urlString?.let { - client.webSocketSession(it) { + logger.debug { "Websocket session initialization started..." } + + session = urlString?.let { url -> + client.webSocketSession(url) { requestBuilder() header(HttpHeaders.SecWebSocketProtocol, MCP_SUBPROTOCOL) @@ -32,5 +37,7 @@ public class WebSocketClientTransport( header(HttpHeaders.SecWebSocketProtocol, MCP_SUBPROTOCOL) } + + logger.debug { "Websocket session initialization finished" } } } diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketMcpKtorClientExtensions.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketMcpKtorClientExtensions.kt index 77062ab..a0df47c 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketMcpKtorClientExtensions.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketMcpKtorClientExtensions.kt @@ -1,10 +1,13 @@ package io.modelcontextprotocol.kotlin.sdk.client +import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.client.HttpClient import io.ktor.client.request.HttpRequestBuilder -import io.modelcontextprotocol.kotlin.sdk.Implementation import io.modelcontextprotocol.kotlin.sdk.LIB_VERSION import io.modelcontextprotocol.kotlin.sdk.shared.IMPLEMENTATION_NAME +import io.modelcontextprotocol.kotlin.sdk.types.Implementation + +private val logger = KotlinLogging.logger {} /** * Returns a new WebSocket transport for the Model Context Protocol using the provided HttpClient. @@ -36,6 +39,8 @@ public suspend fun HttpClient.mcpWebSocket( version = LIB_VERSION, ), ) + logger.debug { "Client started to connect to server" } client.connect(transport) + logger.debug { "Client finished to connect to server" } return client } diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/AbstractClientTransportLifecycleTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/AbstractClientTransportLifecycleTest.kt new file mode 100644 index 0000000..28a1337 --- /dev/null +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/AbstractClientTransportLifecycleTest.kt @@ -0,0 +1,99 @@ +package io.modelcontextprotocol.kotlin.sdk.client + +import io.kotest.assertions.nondeterministic.eventually +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import io.modelcontextprotocol.kotlin.sdk.shared.AbstractClientTransport +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.PingRequest +import io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.CONNECTION_CLOSED +import io.modelcontextprotocol.kotlin.sdk.types.toJSON +import kotlinx.coroutines.delay +import kotlinx.coroutines.test.runTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +abstract class AbstractClientTransportLifecycleTest { + + protected lateinit var transport: T + + @BeforeTest + fun beforeEach() { + transport = createTransport() + } + + @Test + fun `should throw when started twice`() = runTest { + transport.start() + + val exception = shouldThrow { + transport.start() + } + exception.message shouldContain "Can't change state: expected transport state" + } + + @Test + fun `should be idempotent when closed twice`() = runTest { + val transport = createTransport() + + transport.start() + transport.close() + + // Second close should not throw + transport.close() + } + + @Test + fun `should throw when sending before start`() = runTest { + val transport = createTransport() + + val exception = shouldThrow { + transport.send(PingRequest().toJSON()) + } + exception.code shouldBe CONNECTION_CLOSED + exception.message shouldContain "Transport is not ready" + } + + @Test + fun `should throw when sending after close`() = runTest { + val transport = createTransport() + + transport.start() + // Give transport time to initialize + delay(50.milliseconds) + transport.close() + + // Verify that send throws after close using eventually for reliability + eventually(2.seconds) { + shouldThrow { + transport.send(PingRequest().toJSON()) + } + } + } + + @Test + fun `should call onClose exactly once`() = runTest { + val transport = createTransport() + + var closeCallCount = 0 + transport.onClose { closeCallCount++ } + + transport.start() + // Give transport time to initialize + delay(50.milliseconds) + + // Multiple close attempts + transport.close() + transport.close() + + // Verify onClose was called exactly once using eventually for cross-platform reliability + eventually(2.seconds) { + closeCallCount shouldBe 1 + } + } + + protected abstract fun createTransport(): T +} diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientAssertCapabilityTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientAssertCapabilityTest.kt new file mode 100644 index 0000000..e9379a7 --- /dev/null +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientAssertCapabilityTest.kt @@ -0,0 +1,171 @@ +package io.modelcontextprotocol.kotlin.sdk.client + +import io.kotest.matchers.string.shouldContain +import io.modelcontextprotocol.kotlin.sdk.shared.Transport +import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.InitializeResult +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse +import io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertFailsWith + +/** + * Tests for the protected helper [Client.assertCapability]. + * + * `assertCapability(capability, method)` is `protected`, so this suite uses + * a small subclass [TestClient] that re-exports it via [TestClient.exposedAssertCapability]. + * Server capabilities are seeded by completing the initialization handshake through + * [CapabilitiesTransport], which replays a configurable [ServerCapabilities] in its + * `initialize` response. + */ +class ClientAssertCapabilityTest { + + @Test + fun `assertCapability tasks throws when server has no tasks capability`() = runTest { + val client = newTestClient(serverCapabilities = ServerCapabilities()) + val ex = assertFailsWith { + client.exposedAssertCapability("tasks", "tasks/list") + } + ex.message.orEmpty() shouldContain "Server does not support tasks" + } + + @Test + fun `TasksGet throws when server has no tasks capability`() = runTest { + val client = newTestClient(serverCapabilities = ServerCapabilities()) + val ex = assertFailsWith { + client.exposedAssertCapabilityForMethod(Method.Defined.TasksGet) + } + ex.message.orEmpty() shouldContain "Server does not support tasks" + } + + @Test + fun `TasksGet does not throw when server declared tasks`() = runTest { + val client = newTestClient( + serverCapabilities = ServerCapabilities(tasks = ServerCapabilities.Tasks()), + ) + client.exposedAssertCapabilityForMethod(Method.Defined.TasksGet) + } + + @Test + fun `TasksList throws when server tasks list is null`() = runTest { + val client = newTestClient( + serverCapabilities = ServerCapabilities(tasks = ServerCapabilities.Tasks()), + ) + val ex = assertFailsWith { + client.exposedAssertCapabilityForMethod(Method.Defined.TasksList) + } + ex.message.orEmpty() shouldContain "Server does not support listing tasks" + } + + @Test + fun `TasksCancel throws when server tasks cancel is null`() = runTest { + val client = newTestClient( + serverCapabilities = ServerCapabilities(tasks = ServerCapabilities.Tasks()), + ) + val ex = assertFailsWith { + client.exposedAssertCapabilityForMethod(Method.Defined.TasksCancel) + } + ex.message.orEmpty() shouldContain "Server does not support cancelling tasks" + } + + @Test + fun `NotificationsTasksStatus throws when client has no tasks capability`() = runTest { + val client = newTestClient(clientCapabilities = ClientCapabilities()) + val ex = assertFailsWith { + client.exposedAssertNotificationCapability(Method.Defined.NotificationsTasksStatus) + } + ex.message.orEmpty() shouldContain "Client does not support tasks" + } + + @Test + fun `CompletionComplete does not throw when server declared completions`() = runTest { + val client = newTestClient( + serverCapabilities = ServerCapabilities(completions = ServerCapabilities.Completions), + ) + client.exposedAssertCapabilityForMethod(Method.Defined.CompletionComplete) + } + + @Test + fun `CompletionComplete throws when server has no completions capability`() = runTest { + val client = newTestClient( + serverCapabilities = ServerCapabilities(prompts = ServerCapabilities.Prompts()), + ) + val ex = assertFailsWith { + client.exposedAssertCapabilityForMethod(Method.Defined.CompletionComplete) + } + ex.message.orEmpty() shouldContain "Server does not support completions" + } + + private suspend fun newTestClient( + serverCapabilities: ServerCapabilities = ServerCapabilities(), + clientCapabilities: ClientCapabilities = ClientCapabilities(), + ): TestClient { + val client = TestClient( + Implementation("test-client", "1.0.0"), + ClientOptions(capabilities = clientCapabilities), + ) + val transport = CapabilitiesTransport(serverCapabilities) + client.connect(transport) + return client + } + + /** + * Test-only [Client] subclass that exposes the protected + * [Client.assertCapability], [Client.assertCapabilityForMethod], and + * [Client.assertNotificationCapability] helpers to the test code. + */ + private class TestClient(clientInfo: Implementation, options: ClientOptions = ClientOptions()) : + Client(clientInfo, options) { + fun exposedAssertCapability(capability: String, method: String): Unit = assertCapability(capability, method) + fun exposedAssertCapabilityForMethod(method: Method): Unit = assertCapabilityForMethod(method) + fun exposedAssertNotificationCapability(method: Method): Unit = assertNotificationCapability(method) + } + + /** + * Minimal in-memory [Transport] that responds to `initialize` with a configurable + * [ServerCapabilities]. Other JSON-RPC traffic (e.g. the `notifications/initialized` + * sent by [Client.connect]) is silently consumed. + */ + private class CapabilitiesTransport(private val serverCapabilities: ServerCapabilities) : Transport { + private var onMessageBlock: (suspend (JSONRPCMessage) -> Unit)? = null + private var onCloseBlock: (() -> Unit)? = null + + override suspend fun start() = Unit + + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { + if (message is JSONRPCRequest && message.method == "initialize") { + onMessageBlock?.invoke( + JSONRPCResponse( + id = message.id, + result = InitializeResult( + protocolVersion = LATEST_PROTOCOL_VERSION, + capabilities = serverCapabilities, + serverInfo = Implementation("mock-server", "1.0.0"), + ), + ), + ) + } + } + + override suspend fun close() { + onCloseBlock?.invoke() + } + + override fun onMessage(block: suspend (JSONRPCMessage) -> Unit) { + onMessageBlock = block + } + + override fun onClose(block: () -> Unit) { + onCloseBlock = block + } + + override fun onError(block: (Throwable) -> Unit) = Unit + } +} diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientMetaParameterTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientMetaParameterTest.kt new file mode 100644 index 0000000..600859f --- /dev/null +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientMetaParameterTest.kt @@ -0,0 +1,274 @@ +package io.modelcontextprotocol.kotlin.sdk.client + +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonPrimitive +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertContains +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +/** + * Comprehensive test suite for MCP Client meta parameter functionality + * + * Tests cover: + * - Meta key validation according to MCP specification + * - JSON type conversion for various data types + * - Error handling for invalid meta keys + * - Integration with callTool method + */ +class ClientMetaParameterTest { + + private lateinit var client: Client + private lateinit var mockTransport: MockTransport + private val clientInfo = Implementation("test-client", "1.0.0") + + @BeforeTest + fun setup() = runTest { + mockTransport = MockTransport() + client = Client(clientInfo = clientInfo) + mockTransport.setupInitializationResponse() + client.connect(mockTransport) + } + + @Test + fun `should accept valid meta keys without throwing exception`() = runTest { + val validMeta = buildMap { + put("simple-key", "value1") + put("api.example.com/version", "1.0") + put("com.company.app/setting", "enabled") + put("retry_count", 3) + put("user.preference", true) + put("valid123", "alphanumeric") + put("multi.dot.name", "multiple-dots") + put("under_score", "underscore") + put("hyphen-dash", "hyphen") + put("org.apache.kafka/consumer-config", "complex-valid-prefix") + } + + val result = runCatching { + client.callTool("test-tool", mapOf("arg" to "value"), validMeta) + } + + assertTrue(result.isSuccess, "Valid meta keys should not cause exceptions") + mockTransport.lastJsonRpcRequest()?.let { request -> + val params = request.params as JsonObject + assertTrue(params.containsKey("_meta"), "Request should contain _meta field") + val metaField = params["_meta"] as JsonObject + + // Verify all meta keys are present + assertEquals(validMeta.size, metaField.size, "All meta keys should be included") + + // Verify specific key-value pairs + assertEquals("value1", metaField["simple-key"]?.jsonPrimitive?.content) + assertEquals("1.0", metaField["api.example.com/version"]?.jsonPrimitive?.content) + assertEquals("enabled", metaField["com.company.app/setting"]?.jsonPrimitive?.content) + assertEquals(3, metaField["retry_count"]?.jsonPrimitive?.int) + assertEquals(true, metaField["user.preference"]?.jsonPrimitive?.boolean) + assertEquals("alphanumeric", metaField["valid123"]?.jsonPrimitive?.content) + assertEquals("multiple-dots", metaField["multi.dot.name"]?.jsonPrimitive?.content) + assertEquals("underscore", metaField["under_score"]?.jsonPrimitive?.content) + assertEquals("hyphen", metaField["hyphen-dash"]?.jsonPrimitive?.content) + assertEquals("complex-valid-prefix", metaField["org.apache.kafka/consumer-config"]?.jsonPrimitive?.content) + } + } + + @Test + fun `should accept edge case valid prefixes and names`() = runTest { + val edgeCaseValidMeta = buildMap { + put("a/", "single-char-prefix-empty-name") // empty name is allowed + put("a1-b2/test", "alphanumeric-hyphen-prefix") + put("long.domain.name.here/config", "long-prefix") + put("x/a", "minimal-valid-key") + put("test123", "alphanumeric-name-only") + } + + val result = runCatching { + client.callTool("test-tool", emptyMap(), edgeCaseValidMeta) + } + + assertTrue(result.isSuccess, "Edge case valid meta keys should be accepted") + } + + @Test + fun `should reject mcp reserved prefix`() = runTest { + val invalidMeta = mapOf("mcp/internal" to "value") + + val exception = assertFailsWith { + client.callTool("test-tool", emptyMap(), invalidMeta) + } + + assertContains( + charSequence = exception.message ?: "", + other = "Invalid _meta key", + ) + } + + @Test + fun `should reject modelcontextprotocol reserved prefix`() = runTest { + val invalidMeta = mapOf("modelcontextprotocol/config" to "value") + + val exception = assertFailsWith { + client.callTool("test-tool", emptyMap(), invalidMeta) + } + + assertContains( + charSequence = exception.message ?: "", + other = "Invalid _meta key", + ) + } + + @Test + fun `should reject nested reserved prefixes`() = runTest { + val invalidKeys = listOf( + "api.mcp.io/setting", + "com.modelcontextprotocol.test/value", + "example.mcp/data", + "subdomain.mcp.com/config", + "app.modelcontextprotocol.dev/setting", + "test.mcp/value", + "service.modelcontextprotocol/data", + ) + + invalidKeys.forEach { key -> + val exception = assertFailsWith( + message = "Should reject nested reserved key: $key", + ) { + client.callTool("test-tool", emptyMap(), mapOf(key to "value")) + } + assertContains( + charSequence = exception.message ?: "", + other = "Invalid _meta key", + ) + } + } + + @Test + fun `should reject case-insensitive reserved prefixes`() = runTest { + val invalidKeys = listOf( + "MCP/internal", + "Mcp/config", + "mCp/setting", + "MODELCONTEXTPROTOCOL/data", + "ModelContextProtocol/value", + "modelContextProtocol/test", + ) + + invalidKeys.forEach { key -> + val exception = assertFailsWith( + message = "Should reject case-insensitive reserved key: $key", + ) { + client.callTool("test-tool", emptyMap(), mapOf(key to "value")) + } + assertContains( + charSequence = exception.message ?: "", + other = "Invalid _meta key", + ) + } + } + + @Test + fun `should reject invalid key formats`() = runTest { + val invalidKeys = listOf( + "", // empty key - not allowed at key level + "/invalid", // starts with slash + "-invalid", // starts with hyphen + ".invalid", // starts with dot + "in valid", // contains space + "api../test", // consecutive dots + "api./test", // label ends with dot + ) + + invalidKeys.forEach { key -> + assertFailsWith( + message = "Should reject invalid key format: '$key'", + ) { + client.callTool("test-tool", emptyMap(), mapOf(key to "value")) + } + } + } + + @Test + fun `should convert various data types to JSON correctly`() = runTest { + val complexMeta = createComplexMetaData() + + val result = runCatching { + client.callTool( + "test-tool", + emptyMap(), + complexMeta, + ) + } + + assertTrue(result.isSuccess, "Complex data type conversion should not throw exceptions") + + mockTransport.lastJsonRpcRequest()?.let { request -> + assertEquals("tools/call", request.method) + val params = request.params as JsonObject + assertTrue(params.containsKey("_meta"), "Request should contain _meta field") + } + } + + @Test + fun `should handle nested map structures correctly`() = runTest { + val nestedMeta = buildNestedConfiguration() + + val result = runCatching { + client.callTool("test-tool", emptyMap(), nestedMeta) + } + + assertTrue(result.isSuccess) + + mockTransport.lastJsonRpcRequest()?.let { request -> + val params = request.params as JsonObject + val metaField = params["_meta"] as JsonObject + assertTrue(metaField.containsKey("config")) + } + } + + @Test + fun `should include empty meta object when meta parameter not provided`() = runTest { + client.callTool("test-tool", mapOf("arg" to "value")) + + mockTransport.lastJsonRpcRequest()?.let { request -> + val params = request.params as JsonObject + val metaField = params["_meta"] as JsonObject + assertTrue(metaField.isEmpty(), "Meta field should be empty when not provided") + } + } + + private fun createComplexMetaData(): Map = buildMap { + put("string", "text") + put("number", 42) + put("boolean", true) + put("null_value", null) + put("list", listOf(1, 2, 3)) + put("map", mapOf("nested" to "value")) + put("enum", "STRING") + put("int_array", intArrayOf(1, 2, 3)) + } + + private fun buildNestedConfiguration(): Map = buildMap { + put( + "config", + buildMap { + put( + "database", + buildMap { + put("host", "localhost") + put("port", 5432) + }, + ) + put("features", listOf("feature1", "feature2")) + }, + ) + } +} + +suspend fun MockTransport.lastJsonRpcRequest(): JSONRPCRequest? = getSentMessages().lastOrNull() as? JSONRPCRequest diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockTransport.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockTransport.kt new file mode 100644 index 0000000..d7a48ef --- /dev/null +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockTransport.kt @@ -0,0 +1,95 @@ +package io.modelcontextprotocol.kotlin.sdk.client + +import io.modelcontextprotocol.kotlin.sdk.shared.Transport +import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions +import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.InitializeResult +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +class MockTransport : Transport { + private val _sentMessages = mutableListOf() + private val _receivedMessages = mutableListOf() + private val mutex = Mutex() + + suspend fun getSentMessages() = mutex.withLock { _sentMessages.toList() } + suspend fun getReceivedMessages() = mutex.withLock { _receivedMessages.toList() } + + private var onMessageBlock: (suspend (JSONRPCMessage) -> Unit)? = null + private var onCloseBlock: (() -> Unit)? = null + private var onErrorBlock: ((Throwable) -> Unit)? = null + + override suspend fun start() = Unit + + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { + mutex.withLock { + _sentMessages += message + } + + // Auto-respond to initialization and tool calls + when (message) { + is JSONRPCRequest -> { + when (message.method) { + "initialize" -> { + val initResponse = JSONRPCResponse( + id = message.id, + result = InitializeResult( + protocolVersion = "2024-11-05", + capabilities = ServerCapabilities( + tools = ServerCapabilities.Tools(listChanged = null), + ), + serverInfo = Implementation("mock-server", "1.0.0"), + ), + ) + onMessageBlock?.invoke(initResponse) + } + + "tools/call" -> { + val toolResponse = JSONRPCResponse( + id = message.id, + result = CallToolResult( + content = listOf(), + isError = false, + ), + ) + onMessageBlock?.invoke(toolResponse) + } + } + } + + else -> { + // Handle other message types if needed + } + } + } + + override suspend fun close() { + onCloseBlock?.invoke() + } + + override fun onMessage(block: suspend (JSONRPCMessage) -> Unit) { + onMessageBlock = { message -> + mutex.withLock { + _receivedMessages += message + } + block(message) + } + } + + override fun onClose(block: () -> Unit) { + onCloseBlock = block + } + + override fun onError(block: (Throwable) -> Unit) { + onErrorBlock = block + } + + fun setupInitializationResponse() { + // This method helps set up the mock for proper initialization + } +} diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransportTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransportTest.kt deleted file mode 100644 index 4c97272..0000000 --- a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransportTest.kt +++ /dev/null @@ -1,431 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk.client - -import io.ktor.client.HttpClient -import io.ktor.client.engine.mock.MockEngine -import io.ktor.client.engine.mock.MockRequestHandler -import io.ktor.client.engine.mock.respond -import io.ktor.client.plugins.sse.SSE -import io.ktor.http.ContentType -import io.ktor.http.HttpHeaders -import io.ktor.http.HttpMethod -import io.ktor.http.HttpStatusCode -import io.ktor.http.content.TextContent -import io.ktor.http.headersOf -import io.ktor.utils.io.ByteReadChannel -import io.modelcontextprotocol.kotlin.sdk.Implementation -import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage -import io.modelcontextprotocol.kotlin.sdk.JSONRPCNotification -import io.modelcontextprotocol.kotlin.sdk.JSONRPCRequest -import io.modelcontextprotocol.kotlin.sdk.RequestId -import io.modelcontextprotocol.kotlin.sdk.shared.McpJson -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.TimeoutCancellationException -import kotlinx.coroutines.delay -import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeout -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.buildJsonObject -import kotlin.test.Ignore -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNull -import kotlin.test.assertTrue -import kotlin.test.fail -import kotlin.time.Duration.Companion.seconds - -class StreamableHttpClientTransportTest { - - private fun createTransport(handler: MockRequestHandler): StreamableHttpClientTransport { - val mockEngine = MockEngine(handler) - val httpClient = HttpClient(mockEngine) { - install(SSE) { - reconnectionTime = 1.seconds - } - } - - return StreamableHttpClientTransport(httpClient, url = "http://localhost:8080/mcp") - } - - @Test - fun testSendJsonRpcMessage() = runTest { - val message = JSONRPCRequest( - id = RequestId.StringId("test-id"), - method = "test", - params = buildJsonObject { }, - ) - - val transport = createTransport { request -> - assertEquals(HttpMethod.Post, request.method) - assertEquals("http://localhost:8080/mcp", request.url.toString()) - assertEquals(ContentType.Application.Json, request.body.contentType) - - val body = (request.body as TextContent).text - val decodedMessage = McpJson.decodeFromString(body) - assertEquals(message, decodedMessage) - - respond( - content = "", - status = HttpStatusCode.Accepted, - ) - } - - transport.start() - transport.send(message) - transport.close() - } - - @Test - fun testStoreSessionId() = runTest { - val initMessage = JSONRPCRequest( - id = RequestId.StringId("test-id"), - method = "initialize", - params = buildJsonObject { - put( - "clientInfo", - buildJsonObject { - put("name", JsonPrimitive("test-client")) - put("version", JsonPrimitive("1.0")) - }, - ) - put("protocolVersion", JsonPrimitive("2025-06-18")) - }, - ) - - val transport = createTransport { request -> - when (val msg = McpJson.decodeFromString((request.body as TextContent).text)) { - is JSONRPCRequest if msg.method == "initialize" -> respond( - content = "", - status = HttpStatusCode.OK, - headers = headersOf("mcp-session-id", "test-session-id"), - ) - - is JSONRPCNotification if msg.method == "test" -> { - assertEquals("test-session-id", request.headers["mcp-session-id"]) - respond( - content = "", - status = HttpStatusCode.Accepted, - ) - } - - else -> error("Unexpected message: $msg") - } - } - - transport.start() - transport.send(initMessage) - - assertEquals("test-session-id", transport.sessionId) - - transport.send(JSONRPCNotification(method = "test")) - - transport.close() - } - - @Test - fun testTerminateSession() = runTest { -// transport.sessionId = "test-session-id" - - val transport = createTransport { request -> - assertEquals(HttpMethod.Delete, request.method) - assertEquals("test-session-id", request.headers["mcp-session-id"]) - respond( - content = "", - status = HttpStatusCode.OK, - ) - } - - transport.start() - transport.terminateSession() - - assertNull(transport.sessionId) - transport.close() - } - - @Test - fun testTerminateSessionHandle405() = runTest { -// transport.sessionId = "test-session-id" - - val transport = createTransport { request -> - assertEquals(HttpMethod.Delete, request.method) - respond( - content = "", - status = HttpStatusCode.MethodNotAllowed, - ) - } - - transport.start() - // Should not throw for 405 - transport.terminateSession() - - // Session ID should still be cleared - assertNull(transport.sessionId) - transport.close() - } - - @Test - fun testProtocolVersionHeader() = runTest { - val transport = createTransport { request -> - assertEquals("2025-06-18", request.headers["mcp-protocol-version"]) - respond( - content = "", - status = HttpStatusCode.Accepted, - ) - } - transport.protocolVersion = "2025-06-18" - - transport.start() - transport.send(JSONRPCNotification(method = "test")) - transport.close() - } - - // Engine doesn't support SSECapability: https://youtrack.jetbrains.com/issue/KTOR-8177/MockEngine-Add-SSE-support - @Ignore - @Test - fun testNotificationSchemaE2E() = runTest { - val receivedMessages = mutableListOf() - var sseStarted = false - - val transport = createTransport { request -> - when (request.method) { - HttpMethod.Post if request.body.toString().contains("notifications/initialized") -> { - respond( - content = "", - status = HttpStatusCode.Accepted, - headers = headersOf("mcp-session-id", "notification-test-session"), - ) - } - - // Handle SSE connection - HttpMethod.Get -> { - sseStarted = true - val sseContent = buildString { - // Server sends various notifications - appendLine("event: message") - appendLine("id: 1") - appendLine( - """data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}""", - ) - appendLine() - - appendLine("event: message") - appendLine("id: 2") - appendLine("""data: {"jsonrpc":"2.0","method":"notifications/resources/list_changed"}""") - appendLine() - - appendLine("event: message") - appendLine("id: 3") - appendLine("""data: {"jsonrpc":"2.0","method":"notifications/tools/list_changed"}""") - appendLine() - } - respond( - content = ByteReadChannel(sseContent), - status = HttpStatusCode.OK, - headers = headersOf( - HttpHeaders.ContentType, - ContentType.Text.EventStream.toString(), - ), - ) - } - - // Handle regular notifications - HttpMethod.Post -> { - respond( - content = "", - status = HttpStatusCode.Accepted, - ) - } - - else -> respond("", HttpStatusCode.OK) - } - } - - transport.onMessage { message -> - receivedMessages.add(message) - } - - transport.start() - - // Test 1: Send initialized notification to trigger SSE - val initializedNotification = JSONRPCNotification( - method = "notifications/initialized", - params = buildJsonObject { - put("protocolVersion", JsonPrimitive("1.0")) - put( - "capabilities", - buildJsonObject { - put("tools", JsonPrimitive(true)) - put("resources", JsonPrimitive(true)) - }, - ) - }, - ) - - transport.send(initializedNotification) - - // Verify SSE was triggered - assertTrue(sseStarted, "SSE should start after initialized notification") - - // Test 2: Verify received notifications - assertEquals(3, receivedMessages.size) - assertTrue(receivedMessages.all { it is JSONRPCNotification }) - - val notifications = receivedMessages.filterIsInstance() - - // Verify progress notification - val progressNotif = notifications[0] - assertEquals("notifications/progress", progressNotif.method) - val progressParams = progressNotif.params as JsonObject - assertEquals("upload-123", (progressParams["progressToken"] as JsonPrimitive).content) - assertEquals(50, (progressParams["progress"] as JsonPrimitive).content.toInt()) - - // Verify list changed notifications - assertEquals("notifications/resources/list_changed", notifications[1].method) - assertEquals("notifications/tools/list_changed", notifications[2].method) - - // Test 3: Send various client notifications - val clientNotifications = listOf( - JSONRPCNotification( - method = "notifications/progress", - params = buildJsonObject { - put("progressToken", JsonPrimitive("download-456")) - put("progress", JsonPrimitive(75)) - }, - ), - JSONRPCNotification( - method = "notifications/cancelled", - params = buildJsonObject { - put("requestId", JsonPrimitive("req-789")) - put("reason", JsonPrimitive("user_cancelled")) - }, - ), - JSONRPCNotification( - method = "notifications/message", - params = buildJsonObject { - put("level", JsonPrimitive("info")) - put("message", JsonPrimitive("Operation completed")) - put( - "data", - buildJsonObject { - put("duration", JsonPrimitive(1234)) - }, - ) - }, - ), - ) - - // Send all client notifications - clientNotifications.forEach { notification -> - transport.send(notification) - } - - // Verify session ID is maintained - assertEquals("notification-test-session", transport.sessionId) - transport.close() - } - - // Engine doesn't support SSECapability: https://youtrack.jetbrains.com/issue/KTOR-8177/MockEngine-Add-SSE-support - @Ignore - @Test - fun testNotificationWithResumptionToken() = runTest { - var resumptionTokenReceived: String? = null - var lastEventIdSent: String? = null - - val transport = createTransport { request -> - // Capture Last-Event-ID header - lastEventIdSent = request.headers["Last-Event-ID"] - - when (request.method) { - HttpMethod.Get -> { - val sseContent = buildString { - appendLine("event: message") - appendLine("id: resume-100") - appendLine( - """data: {"jsonrpc":"2.0","method":"notifications/resumed","params":{"fromToken":"$lastEventIdSent"}}""", - ) - appendLine() - } - respond( - content = ByteReadChannel(sseContent), - status = HttpStatusCode.OK, - headers = headersOf( - HttpHeaders.ContentType, - ContentType.Text.EventStream.toString(), - ), - ) - } - - else -> respond("", HttpStatusCode.Accepted) - } - } - - transport.start() - - // Send notification with resumption token - transport.send( - message = JSONRPCNotification( - method = "notifications/test", - params = buildJsonObject { - put("data", JsonPrimitive("test-data")) - }, - ), - resumptionToken = "previous-token-99", - onResumptionToken = { token -> - resumptionTokenReceived = token - }, - ) - - // Wait for response - delay(1.seconds) - - // Verify resumption token was sent in header - assertEquals("previous-token-99", lastEventIdSent) - - // Verify new resumption token was received - assertEquals("resume-100", resumptionTokenReceived) - transport.close() - } - - @Test - fun testClientConnectWithInvalidJson() = runTest { - // Transport under test: respond with invalid JSON for the initialize request - val transport = createTransport { _ -> - respond( - "this is not valid json", - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()), - ) - } - - val client = Client( - clientInfo = Implementation( - name = "test-client", - version = "1.0", - ), - ) - - runCatching { - // Real time-keeping is needed; otherwise Protocol will always throw TimeoutCancellationException in tests - withContext(Dispatchers.Default.limitedParallelism(1)) { - withTimeout(5.seconds) { - client.connect(transport) - } - } - }.onSuccess { - fail("Expected client.connect to fail on invalid JSON response") - }.onFailure { e -> - when (e) { - is TimeoutCancellationException -> fail("Client connect caused a hang", e) - - is IllegalStateException -> { - // Expected behavior: connect finishes and fails with an exception. - } - - else -> fail("Unexpected exception during client.connect", e) - } - }.also { - transport.close() - } - } -} diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/channel/ChannelClientTransportLifecycleTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/channel/ChannelClientTransportLifecycleTest.kt new file mode 100644 index 0000000..853a433 --- /dev/null +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/channel/ChannelClientTransportLifecycleTest.kt @@ -0,0 +1,20 @@ +package io.modelcontextprotocol.kotlin.sdk.client.channel + +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.client.AbstractClientTransportLifecycleTest +import io.modelcontextprotocol.kotlin.sdk.testing.ChannelTransport +import kotlin.test.Ignore +import kotlin.test.Test + +@OptIn(ExperimentalMcpApi::class) +class ChannelClientTransportLifecycleTest : AbstractClientTransportLifecycleTest() { + + /** + * Dummy method to make IDE treat this class as a test + */ + @Test + @Ignore + fun dummyTest() = Unit + + override fun createTransport(): ChannelTransport = ChannelTransport() +} diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/MockSseClientEngine.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/MockSseClientEngine.kt new file mode 100644 index 0000000..14af546 --- /dev/null +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/MockSseClientEngine.kt @@ -0,0 +1,107 @@ +package io.modelcontextprotocol.kotlin.sdk.client.sse + +import io.ktor.client.engine.HttpClientEngine +import io.ktor.client.engine.HttpClientEngineConfig +import io.ktor.client.plugins.sse.SSECapability +import io.ktor.client.request.HttpRequestData +import io.ktor.client.request.HttpResponseData +import io.ktor.client.request.ResponseAdapterAttributeKey +import io.ktor.http.Headers +import io.ktor.http.HttpMethod +import io.ktor.http.HttpProtocolVersion +import io.ktor.http.HttpStatusCode +import io.ktor.http.headersOf +import io.ktor.util.date.GMTDate +import io.ktor.utils.io.ByteChannel +import io.ktor.utils.io.ByteReadChannel +import io.ktor.utils.io.InternalAPI +import io.ktor.utils.io.writeStringUtf8 +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlin.coroutines.CoroutineContext + +@OptIn(InternalAPI::class) +internal open class MockSseClientEngine( + private val endpoint: String, + private val onPostRequest: (postRequest: HttpRequestData) -> Unit, + override val dispatcher: CoroutineDispatcher = Dispatchers.Default, +) : HttpClientEngine { + + override val config = HttpClientEngineConfig() + override val supportedCapabilities = setOf(SSECapability) + + private val job = SupervisorJob() + override val coroutineContext: CoroutineContext = dispatcher + job + + private val channel by lazy { ByteChannel(true) } + + override suspend fun execute(data: HttpRequestData): HttpResponseData = when (data.method) { + HttpMethod.Get -> respondWithSseEndpointEvent(data) + + HttpMethod.Post -> { + onPostRequest(data) + respondWithAccepted() + } + + else -> error("Unsupported method: ${data.method}") + } + + override fun close() { + channel.close() + job.cancel() + } + + /** + * Closes the SSE byte channel without disposing the engine, simulating a server-side + * disconnect of the SSE stream. + */ + fun disconnectSseStream() { + channel.close() + } + + private fun responseContext(): CoroutineContext = dispatcher + Job() + + private suspend fun respondWithSseEndpointEvent(data: HttpRequestData): HttpResponseData { + val sseResponseAdapter = requireNotNull(data.attributes.getOrNull(ResponseAdapterAttributeKey)) { + "Missing SSE response adapter." + } + channel.writeStringUtf8(endpointEvent()) + channel.flush() + val responseContext = responseContext() + val responseBody = sseResponseAdapter.adapt( + data, + HttpStatusCode.OK, + eventStreamHeader, + channel, + data.body, + responseContext, + ) ?: error("Failed to adapt SSE response.") + return HttpResponseData( + statusCode = HttpStatusCode.OK, + requestTime = GMTDate(), + headers = eventStreamHeader, + version = HttpProtocolVersion.HTTP_1_1, + body = responseBody, + callContext = responseContext, + ) + } + + private val eventStreamHeader by lazy { headersOf("Content-Type", "text/event-stream") } + + private fun endpointEvent(): String = buildString { + appendLine("event: endpoint") + appendLine("data: $endpoint") + appendLine() + } + + private fun respondWithAccepted(): HttpResponseData = HttpResponseData( + statusCode = HttpStatusCode.Accepted, + requestTime = GMTDate(), + headers = Headers.Empty, + version = HttpProtocolVersion.HTTP_1_1, + body = ByteReadChannel.Empty, + callContext = responseContext(), + ) +} diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/SseClientTransportTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/SseClientTransportTest.kt new file mode 100644 index 0000000..319ba2a --- /dev/null +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/SseClientTransportTest.kt @@ -0,0 +1,133 @@ +package io.modelcontextprotocol.kotlin.sdk.client.sse + +import io.kotest.assertions.nondeterministic.eventually +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import io.ktor.client.HttpClient +import io.ktor.client.plugins.sse.SSE +import io.ktor.client.request.HttpRequestData +import io.modelcontextprotocol.kotlin.sdk.client.SseClientTransport +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.time.Duration.Companion.seconds + +class SseClientTransportTest { + + @Test + fun `absolute path endpoint resolves against origin`() = runTest { + // Given + val sseUrl = "http://example.com/api/mcp/sse" + + // And + val endpointEvent = "/messages?sessionId=abc" + + // And + val engine = CapturingSseClientEngine(endpoint = endpointEvent) + val transport = sseTransport(sseUrl, engine) + + // When + transport.start() + transport.send(JSONRPCNotification(method = "test")) + + // Then + val capturedPosts = engine.capturedPosts + capturedPosts shouldHaveSize 1 + capturedPosts[0].url.toString() shouldBe "http://example.com/messages?sessionId=abc" + + // Cleanup + transport.close() + engine.close() + } + + @Test + fun `relative path endpoint resolves against baseUrl`() = runTest { + // Given + val sseUrl = "http://example.com/api/mcp/sse" + + // And + val endpointEvent = "post?sessionId=xyz" + + // And + val engine = CapturingSseClientEngine(endpoint = endpointEvent) + val transport = sseTransport(sseUrl, engine) + + // When + transport.start() + transport.send(JSONRPCNotification(method = "test")) + + // Then + val capturedPosts = engine.capturedPosts + capturedPosts shouldHaveSize 1 + capturedPosts[0].url.toString() shouldBe "http://example.com/api/mcp/post?sessionId=xyz" + + // Cleanup + transport.close() + engine.close() + } + + @Test + fun `full url endpoint is used as-is`() = runTest { + // Given + val sseUrl = "http://example.com/api/mcp/sse" + + // And + val endpointEvent = "https://example.com/messages?sessionId=abc" + + // And + val engine = CapturingSseClientEngine(endpoint = endpointEvent) + val transport = sseTransport(sseUrl, engine) + + // When + transport.start() + transport.send(JSONRPCNotification(method = "test")) + + // Then + val capturedPosts = engine.capturedPosts + capturedPosts shouldHaveSize 1 + capturedPosts[0].url.toString() shouldBe "https://example.com/messages?sessionId=abc" + + // Cleanup + transport.close() + engine.close() + } + + @Test + fun `onClose callback fires when the SSE stream is disconnected by the server`() = runTest { + // Given + val sseUrl = "http://example.com/api/mcp/sse" + + // And + val engine = CapturingSseClientEngine(endpoint = "/messages?sessionId=abc") + val transport = sseTransport(sseUrl, engine) + var onCloseFired = false + transport.onClose { onCloseFired = true } + + // When + transport.start() + engine.disconnectSseStream() + + // Then + eventually(2.seconds) { + onCloseFired shouldBe true + } + + // Cleanup + transport.close() + engine.close() + } + + private fun sseTransport(sseUrl: String, engine: CapturingSseClientEngine) = + SseClientTransport(HttpClient(engine) { install(SSE) }, sseUrl) + + private class CapturingSseClientEngine private constructor( + endpoint: String, + private val capturedPostRequests: MutableList = mutableListOf(), + ) : MockSseClientEngine(endpoint, capturedPostRequests::add) { + + constructor(endpoint: String) : this(endpoint, mutableListOf()) + + val capturedPosts: List + get() = capturedPostRequests + } +} diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/stdio/StdioClientTransportLifecycleTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/stdio/StdioClientTransportLifecycleTest.kt new file mode 100644 index 0000000..6354242 --- /dev/null +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/stdio/StdioClientTransportLifecycleTest.kt @@ -0,0 +1,26 @@ +package io.modelcontextprotocol.kotlin.sdk.client.stdio + +import io.modelcontextprotocol.kotlin.sdk.client.AbstractClientTransportLifecycleTest +import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport +import kotlinx.io.Buffer +import kotlin.test.Ignore +import kotlin.test.Test + +class StdioClientTransportLifecycleTest : AbstractClientTransportLifecycleTest() { + + /** + * Dummy method to make IDE treat this class as a test + */ + @Test + @Ignore + fun dummyTest() = Unit + + override fun createTransport(): StdioClientTransport { + val inputBuffer = Buffer() + val outputBuffer = Buffer() + return StdioClientTransport( + input = inputBuffer, + output = outputBuffer, + ) + } +} diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt new file mode 100644 index 0000000..d9aaf7a --- /dev/null +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt @@ -0,0 +1,1150 @@ +package io.modelcontextprotocol.kotlin.sdk.client.streamable.http + +import io.kotest.assertions.fail +import io.kotest.assertions.nondeterministic.eventually +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.ktor.client.HttpClient +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.MockRequestHandler +import io.ktor.client.engine.mock.respond +import io.ktor.client.plugins.sse.SSE +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpMethod +import io.ktor.http.HttpStatusCode +import io.ktor.http.content.TextContent +import io.ktor.http.headersOf +import io.ktor.utils.io.ByteReadChannel +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport +import io.modelcontextprotocol.kotlin.sdk.shared.TooLongFrameException +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.McpJson +import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import io.modelcontextprotocol.kotlin.sdk.types.RequestId +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +class StreamableHttpClientTransportTest { + + private fun createTransport( + maxInlineSseEventSize: Int = 16 * 1024 * 1024, + handler: MockRequestHandler, + ): StreamableHttpClientTransport { + val mockEngine = MockEngine(handler) + val httpClient = HttpClient(mockEngine) { + install(SSE) { + reconnectionTime = 1.seconds + } + } + + return StreamableHttpClientTransport( + httpClient, + url = "http://localhost:8080/mcp", + maxInlineSseEventSize = maxInlineSseEventSize, + ) + } + + private fun buildSseMessage(id: String, method: String, params: String): String = buildString { + appendLine("event: message") + appendLine("id: $id") + val data = """{"jsonrpc":"2.0","method":"$method","params":$params}""" + appendLine("data: $data") + appendLine() + } + + @Test + fun testSendJsonRpcMessage() = runTest { + val message = JSONRPCRequest( + id = "test-id", + method = "test", + params = buildJsonObject { }, + ) + + val transport = createTransport { request -> + assertEquals(HttpMethod.Post, request.method) + assertEquals("http://localhost:8080/mcp", request.url.toString()) + assertEquals(ContentType.Application.Json, request.body.contentType) + + val body = (request.body as TextContent).text + val decodedMessage = McpJson.decodeFromString(body) + assertEquals(message, decodedMessage) + + respond( + content = "", + status = HttpStatusCode.Accepted, + ) + } + + transport.start() + transport.send(message) + transport.close() + } + + @Test + fun testStartTransportTwoTimesThrowsException() = runTest { + val transport = createTransport { _ -> + respond( + content = "", + status = HttpStatusCode.Accepted, + ) + } + + transport.start() + try { + transport.start() + } catch (e: IllegalStateException) { + e shouldBe IllegalStateException("Can't change state: expected transport state New, but found Operational.") + } + transport.close() + } + + @Test + fun testCloseTransportTwoTimesIsPossible() = runTest { + val transport = createTransport { _ -> + respond( + content = "", + status = HttpStatusCode.Accepted, + ) + } + + transport.start() + transport.close() + try { + transport.close() + } catch (_: Exception) { + fail("We expect no exceptions when closing already closed transport") + } + } + + @Test + fun testStoreSessionId() = runTest { + val initMessage = JSONRPCRequest( + id = "test-id", + method = "initialize", + params = buildJsonObject { + put( + "clientInfo", + buildJsonObject { + put("name", JsonPrimitive("test-client")) + put("version", JsonPrimitive("1.0")) + }, + ) + put("protocolVersion", JsonPrimitive("2025-06-18")) + }, + ) + + val transport = createTransport { request -> + val msg = McpJson.decodeFromString( + (request.body as TextContent).text, + ) + when { + msg is JSONRPCRequest && msg.method == "initialize" -> respond( + content = "", + status = HttpStatusCode.OK, + headers = headersOf("mcp-session-id", "test-session-id"), + ) + + msg is JSONRPCNotification && msg.method == "test" -> { + assertEquals("test-session-id", request.headers["mcp-session-id"]) + respond( + content = "", + status = HttpStatusCode.Accepted, + ) + } + + else -> error("Unexpected message: $msg") + } + } + + transport.start() + transport.send(initMessage) + + assertEquals("test-session-id", transport.sessionId) + + transport.send(JSONRPCNotification(method = "test")) + + transport.close() + } + + @Test + fun testTerminateSession() = runTest { +// transport.sessionId = "test-session-id" + + val transport = createTransport { request -> + assertEquals(HttpMethod.Delete, request.method) + assertEquals("test-session-id", request.headers["mcp-session-id"]) + respond( + content = "", + status = HttpStatusCode.OK, + ) + } + + transport.start() + transport.terminateSession() + + assertNull(transport.sessionId) + transport.close() + } + + @Test + fun testTerminateSessionHandle405() = runTest { +// transport.sessionId = "test-session-id" + + val transport = createTransport { request -> + assertEquals(HttpMethod.Delete, request.method) + respond( + content = "", + status = HttpStatusCode.MethodNotAllowed, + ) + } + + transport.start() + // Should not throw for 405 + transport.terminateSession() + + // Session ID should still be cleared + assertNull(transport.sessionId) + transport.close() + } + + @Test + fun testProtocolVersionHeader() = runTest { + val transport = createTransport { request -> + assertEquals("2025-06-18", request.headers["mcp-protocol-version"]) + respond( + content = "", + status = HttpStatusCode.Accepted, + ) + } + transport.protocolVersion = "2025-06-18" + + transport.start() + transport.send(JSONRPCNotification(method = "test")) + transport.close() + } + + @Test + fun `should send MCP method and name headers from name`() = runTest { + val transport = createTransport { request -> + assertEquals("tools/call", request.headers["Mcp-Method"]) + assertEquals("read_file", request.headers["Mcp-Name"]) + respond(content = "", status = HttpStatusCode.Accepted) + } + + transport.start() + transport.send( + JSONRPCRequest( + id = "test-id", + method = "tools/call", + params = buildJsonObject { + put("name", JsonPrimitive("read_file")) + put("arguments", buildJsonObject { put("path", JsonPrimitive("README.md")) }) + }, + ), + ) + transport.close() + } + + @Test + fun `should send MCP name header from URI`() = runTest { + val transport = createTransport { request -> + assertEquals("resources/read", request.headers["Mcp-Method"]) + assertEquals("file:///README.md", request.headers["Mcp-Name"]) + respond(content = "", status = HttpStatusCode.Accepted) + } + + transport.start() + transport.send( + JSONRPCRequest( + id = "test-id", + method = "resources/read", + params = buildJsonObject { put("uri", JsonPrimitive("file:///README.md")) }, + ), + ) + transport.close() + } + + @Test + fun `should encode MCP name header values when required`() = runTest { + val cases = listOf( + "Hello, 世界" to "=?base64?SGVsbG8sIOS4lueVjA==?=", + " padded " to "=?base64?IHBhZGRlZCA=?=", + "line1\nline2" to "=?base64?bGluZTEKbGluZTI=?=", + "=?base64?literal?=" to "=?base64?PT9iYXNlNjQ/bGl0ZXJhbD89?=", + ) + + cases.forEachIndexed { index, (name, expectedHeader) -> + val transport = createTransport { request -> + assertEquals(expectedHeader, request.headers["Mcp-Name"]) + respond(content = "", status = HttpStatusCode.Accepted) + } + + transport.start() + transport.send( + JSONRPCRequest( + id = "test-id-$index", + method = "tools/call", + params = buildJsonObject { put("name", JsonPrimitive(name)) }, + ), + ) + transport.close() + } + } + + @Test + fun `should send MCP method header for notifications`() = runTest { + val transport = createTransport { request -> + assertEquals("notifications/tools/list_changed", request.headers["Mcp-Method"]) + assertNull(request.headers["Mcp-Name"]) + respond(content = "", status = HttpStatusCode.Accepted) + } + + transport.start() + transport.send(JSONRPCNotification(method = "notifications/tools/list_changed")) + transport.close() + } + + @Test + fun `should omit MCP standard headers for responses`() = runTest { + val transport = createTransport { request -> + assertNull(request.headers["Mcp-Method"]) + assertNull(request.headers["Mcp-Name"]) + respond(content = "", status = HttpStatusCode.Accepted) + } + + transport.start() + transport.send(JSONRPCResponse(id = RequestId.StringId("test-id"))) + transport.close() + } + + // Engine doesn't support SSECapability: https://youtrack.jetbrains.com/issue/KTOR-8177/MockEngine-Add-SSE-support + @Ignore + @Test + fun testNotificationSchemaE2E() = runTest { + val receivedMessages = mutableListOf() + var sseStarted = false + + val transport = createTransport { request -> + when { + request.method == HttpMethod.Post && request.body.toString().contains("notifications/initialized") -> { + respond( + content = "", + status = HttpStatusCode.Accepted, + headers = headersOf("mcp-session-id", "notification-test-session"), + ) + } + + // Handle SSE connection + request.method == HttpMethod.Get -> { + sseStarted = true + val sseContent = buildString { + // Server sends various notifications + appendLine("event: message") + appendLine("id: 1") + appendLine( + """data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}""", + ) + appendLine() + + appendLine("event: message") + appendLine("id: 2") + appendLine("""data: {"jsonrpc":"2.0","method":"notifications/resources/list_changed"}""") + appendLine() + + appendLine("event: message") + appendLine("id: 3") + appendLine("""data: {"jsonrpc":"2.0","method":"notifications/tools/list_changed"}""") + appendLine() + } + respond( + content = ByteReadChannel(sseContent), + status = HttpStatusCode.OK, + headers = headersOf( + HttpHeaders.ContentType, + ContentType.Text.EventStream.toString(), + ), + ) + } + + // Handle regular notifications + request.method == HttpMethod.Post -> { + respond( + content = "", + status = HttpStatusCode.Accepted, + ) + } + + else -> respond("", HttpStatusCode.OK) + } + } + + transport.onMessage { message -> + receivedMessages.add(message) + } + + transport.start() + + // Test 1: Send initialized notification to trigger SSE + val initializedNotification = JSONRPCNotification( + method = "notifications/initialized", + params = buildJsonObject { + put("protocolVersion", JsonPrimitive("1.0")) + put( + "capabilities", + buildJsonObject { + put("tools", JsonPrimitive(true)) + put("resources", JsonPrimitive(true)) + }, + ) + }, + ) + + transport.send(initializedNotification) + + // Verify SSE was triggered + assertTrue(sseStarted, "SSE should start after initialized notification") + + // Test 2: Verify received notifications + receivedMessages shouldHaveSize 3 + val notifications = receivedMessages.filterIsInstance() + notifications shouldHaveSize 3 + + // Verify progress notification + val progressNotif = notifications[0] + assertEquals("notifications/progress", progressNotif.method) + val progressParams = progressNotif.params as JsonObject + assertEquals("upload-123", (progressParams["progressToken"] as JsonPrimitive).content) + assertEquals(50, (progressParams["progress"] as JsonPrimitive).content.toInt()) + + // Verify list changed notifications + assertEquals("notifications/resources/list_changed", notifications[1].method) + assertEquals("notifications/tools/list_changed", notifications[2].method) + + // Test 3: Send various client notifications + val clientNotifications = listOf( + JSONRPCNotification( + method = "notifications/progress", + params = buildJsonObject { + put("progressToken", JsonPrimitive("download-456")) + put("progress", JsonPrimitive(75)) + }, + ), + JSONRPCNotification( + method = "notifications/cancelled", + params = buildJsonObject { + put("requestId", JsonPrimitive("req-789")) + put("reason", JsonPrimitive("user_cancelled")) + }, + ), + JSONRPCNotification( + method = "notifications/message", + params = buildJsonObject { + put("level", JsonPrimitive("info")) + put("message", JsonPrimitive("Operation completed")) + put( + "data", + buildJsonObject { + put("duration", JsonPrimitive(1234)) + }, + ) + }, + ), + ) + + // Send all client notifications + clientNotifications.forEach { notification -> + transport.send(notification) + } + + // Verify session ID is maintained + assertEquals("notification-test-session", transport.sessionId) + transport.close() + } + + // Engine doesn't support SSECapability: https://youtrack.jetbrains.com/issue/KTOR-8177/MockEngine-Add-SSE-support + @Ignore + @Test + fun testNotificationWithResumptionToken() = runTest { + var resumptionTokenReceived: String? = null + var lastEventIdSent: String? = null + + val transport = createTransport { request -> + // Capture Last-Event-ID header + lastEventIdSent = request.headers["Last-Event-ID"] + + when (request.method) { + HttpMethod.Get -> { + val sseContent = buildString { + appendLine("event: message") + appendLine("id: resume-100") + appendLine( + """data: {"jsonrpc":"2.0","method":"notifications/resumed","params":{"fromToken":"$lastEventIdSent"}}""", + ) + appendLine() + } + respond( + content = ByteReadChannel(sseContent), + status = HttpStatusCode.OK, + headers = headersOf( + HttpHeaders.ContentType, + ContentType.Text.EventStream.toString(), + ), + ) + } + + else -> respond("", HttpStatusCode.Accepted) + } + } + + transport.start() + + // Send notification with resumption token + transport.send( + message = JSONRPCNotification( + method = "notifications/test", + params = buildJsonObject { + put("data", JsonPrimitive("test-data")) + }, + ), + resumptionToken = "previous-token-99", + onResumptionToken = { token -> + resumptionTokenReceived = token + }, + ) + + // Wait for response + delay(1.seconds) + + // Verify resumption token was sent in header + assertEquals("previous-token-99", lastEventIdSent) + + // Verify new resumption token was received + assertEquals("resume-100", resumptionTokenReceived) + transport.close() + } + + @Test + fun testClientConnectWithInvalidJson() = runTest { + // Transport under test: respond with invalid JSON for the initialize request + val transport = createTransport { _ -> + respond( + "this is not valid json", + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()), + ) + } + + val client = Client( + clientInfo = Implementation( + name = "test-client", + version = "1.0", + ), + ) + + try { + // Real time-keeping is needed; otherwise Protocol will always throw TimeoutCancellationException in tests + val mcpException = assertFailsWith( + message = "Expected client.connect to fail on invalid JSON response", + ) { + withContext(Dispatchers.Default.limitedParallelism(1)) { + withTimeout(5.seconds) { + client.connect(transport) + } + } + } + mcpException.code shouldBe RPCError.ErrorCode.INTERNAL_ERROR + } finally { + transport.close() + } + } + + @Test + fun testSkipEmptySSE() = runTest { + val transport = createTransport { request -> + if (request.method == HttpMethod.Post) { + val sseContent = buildString { + // Event 1: empty data — must be skipped without error + appendLine("id: a") + appendLine("data:") + appendLine() + // Event 2: whitespace-only data — must be skipped without error + appendLine("id: b") + appendLine("data: \t ") + appendLine() + // Event 3: multi-line data lines that together form a valid JSON-RPC response + appendLine("id: c") + appendLine("event: message") + appendLine("data: {") + appendLine("""data: "jsonrpc":"2.0",""") + appendLine("""data: "id":"test-1",""") + appendLine("""data: "result":{""") + appendLine("""data: "protocolVersion":"2025-06-18",""") + appendLine("""data: "capabilities":{},""") + appendLine("""data: "serverInfo":{"name":"server","version":"1.0.0"}""") + appendLine("data: }") + appendLine("data: }") + appendLine() + } + + respond( + content = ByteReadChannel(sseContent), + status = HttpStatusCode.OK, + headers = headersOf( + HttpHeaders.ContentType, + ContentType.Text.EventStream.toString(), + ), + ) + } else { + respond("", HttpStatusCode.OK) + } + } + + val receivedMessages = mutableListOf() + val messageReceived = CompletableDeferred() + + transport.onMessage { message -> + receivedMessages.add(message) + if (!messageReceived.isCompleted) { + messageReceived.complete(Unit) + } + } + + transport.start() + + transport.send( + JSONRPCRequest( + id = "test-1", + method = "initialize", + params = buildJsonObject { }, + ), + ) + + eventually { + messageReceived.await() + } + + receivedMessages shouldHaveSize 1 + val response = receivedMessages[0] + response.shouldBeInstanceOf() + response.id shouldBe RequestId.StringId("test-1") + + transport.close() + } + + @Test + fun testInlineSseRejectsEventExceedingMaxSize() = runTest { + // A malicious server streams an endless single event: many `data:` lines that accumulate + // past the cap and never send the blank-line terminator that would flush the buffer. + val maxInlineSseEventSize = 64 + val transport = createTransport(maxInlineSseEventSize) { request -> + if (request.method == HttpMethod.Post) { + val sseContent = buildString { + appendLine("event: message") + // 20 lines × 16 chars = 320 chars of accumulated data, no terminating blank line. + repeat(20) { appendLine("data: ${"A".repeat(16)}") } + } + respond( + content = ByteReadChannel(sseContent), + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, ContentType.Text.EventStream.toString()), + ) + } else { + respond("", HttpStatusCode.OK) + } + } + + val receivedMessages = mutableListOf() + val receivedErrors = mutableListOf() + transport.onMessage { receivedMessages.add(it) } + transport.onError { receivedErrors.add(it) } + transport.start() + + val error = assertFailsWith { + transport.send(JSONRPCRequest(id = "req-1", method = "test", params = buildJsonObject { })) + } + + error.cause.shouldBeInstanceOf() + receivedErrors.filterIsInstance() shouldHaveSize 1 + receivedMessages shouldHaveSize 0 + transport.close() + } + + @Test + fun testInlineSseEventExactlyAtMaxSizeIsAccepted() = runTest { + // An event whose assembled data length equals the cap must still be accepted and dispatched: + // the guard rejects only sizes strictly greater than the cap (parity with ReadBuffer). + val part1 = """{"jsonrpc":"2.0",""" + val part2 = """"method":"notifications/tools/list_changed"}""" + val maxInlineSseEventSize = (part1 + part2).length + + val transport = createTransport(maxInlineSseEventSize) { request -> + if (request.method == HttpMethod.Post) { + val sseContent = buildString { + appendLine("event: message") + appendLine("data: $part1") + appendLine("data: $part2") + appendLine() + } + respond( + content = ByteReadChannel(sseContent), + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, ContentType.Text.EventStream.toString()), + ) + } else { + respond("", HttpStatusCode.OK) + } + } + + val receivedMessages = mutableListOf() + val receivedErrors = mutableListOf() + val messageReceived = CompletableDeferred() + transport.onMessage { + receivedMessages.add(it) + if (!messageReceived.isCompleted) messageReceived.complete(Unit) + } + transport.onError { receivedErrors.add(it) } + transport.start() + + transport.send(JSONRPCRequest(id = "req-1", method = "test", params = buildJsonObject { })) + + eventually { messageReceived.await() } + + receivedMessages shouldHaveSize 1 + (receivedMessages[0] as JSONRPCNotification).method shouldBe "notifications/tools/list_changed" + receivedErrors shouldHaveSize 0 + transport.close() + } + + @Test + fun testNonPositiveMaxInlineSseEventSizeThrows() { + assertFailsWith { + createTransport(maxInlineSseEventSize = 0) { respond("", HttpStatusCode.OK) } + } + } + + @Test + fun testInlineSSEInResponse() = runTest { + val transport = createTransport { request -> + if (request.method == HttpMethod.Post) { + val sseContent = buildString { + append( + buildSseMessage( + id = "1", + method = "notifications/progress", + params = """{"progressToken":"task-1","progress":50}""", + ), + ) + append( + buildSseMessage( + id = "2", + method = "notifications/tools/list_changed", + params = "{}", + ), + ) + } + + respond( + content = ByteReadChannel(sseContent), + status = HttpStatusCode.OK, + headers = headersOf( + HttpHeaders.ContentType, + ContentType.Text.EventStream.toString(), + ), + ) + } else { + respond("", HttpStatusCode.OK) + } + } + + val receivedMessages = mutableListOf() + val twoMessagesReceived = CompletableDeferred() + + transport.onMessage { message -> + receivedMessages.add(message) + if (receivedMessages.size >= 2 && !twoMessagesReceived.isCompleted) { + twoMessagesReceived.complete(Unit) + } + } + + transport.start() + + transport.send( + JSONRPCRequest( + id = "test-1", + method = "test", + params = buildJsonObject { }, + ), + ) + + eventually { + twoMessagesReceived.await() + } + + receivedMessages shouldHaveSize 2 + + val firstNotification = receivedMessages[0] as JSONRPCNotification + firstNotification.method shouldBe "notifications/progress" + + val secondNotification = receivedMessages[1] as JSONRPCNotification + secondNotification.method shouldBe "notifications/tools/list_changed" + + transport.close() + } + + @Test + fun testErrorInSSEResponse() = runTest { + val errorMessage = "Something went wrong on the server" + val transport = createTransport { request -> + if (request.method == HttpMethod.Post) { + val sseContent = buildString { + appendLine("event: error") + appendLine("data: $errorMessage") + appendLine() + } + + respond( + content = ByteReadChannel(sseContent), + status = HttpStatusCode.OK, + headers = headersOf( + HttpHeaders.ContentType, + ContentType.Text.EventStream.toString(), + ), + ) + } else { + respond("", HttpStatusCode.OK) + } + } + + val receivedErrors = mutableListOf() + val errorDeferred = CompletableDeferred() + + transport.onError { error -> + receivedErrors.add(error) + if (!errorDeferred.isCompleted) { + errorDeferred.complete(error) + } + } + + transport.start() + + transport.send( + JSONRPCRequest( + id = "test-error", + method = "test", + params = buildJsonObject { }, + ), + ) + + val error = eventually { errorDeferred.await() } + + receivedErrors.size shouldBe 1 + error.message shouldBe "Streamable HTTP error: $errorMessage" + + transport.close() + } + + @Test + fun testCloseTransportDuringEventStream() = runTest { + val transport = createTransport { request -> + if (request.method == HttpMethod.Post) { + val sseContent = buildString { + append( + buildSseMessage( + id = "1", + method = "notifications/progress", + params = """{"progressToken":"task-1","progress":25}""", + ), + ) + append( + buildSseMessage( + id = "2", + method = "notifications/progress", + params = """{"progressToken":"task-1","progress":50}""", + ), + ) + append( + buildSseMessage( + id = "3", + method = "notifications/progress", + params = """{"progressToken":"task-1","progress":75}""", + ), + ) + } + respond( + content = ByteReadChannel(sseContent), + status = HttpStatusCode.OK, + headers = headersOf( + HttpHeaders.ContentType, + ContentType.Text.EventStream.toString(), + ), + ) + } else { + respond("", HttpStatusCode.OK) + } + } + + val (receivedMessages, receivedErrors) = setupTransportAndCollectMessages(transport) + + eventually { + while (receivedMessages.isEmpty()) { + delay(10.milliseconds) + } + } + + transport.close() + + receivedMessages shouldHaveSize 3 + + receivedMessages.forEach { message -> + message.shouldBeInstanceOf() + message.method shouldBe "notifications/progress" + } + receivedErrors shouldHaveSize 0 + } + + @Test + fun testInlineSseRetryParsing() = runTest { + val transport = createTransport { request -> + if (request.method == HttpMethod.Post) { + val sseContent = buildString { + appendLine("retry: 5000") + appendLine("id: ev-1") + appendLine("event: message") + appendLine("""data: {"jsonrpc":"2.0","id":"req-1","result":{"tools":[]}}""") + appendLine() + } + + respond( + content = ByteReadChannel(sseContent), + status = HttpStatusCode.OK, + headers = headersOf( + HttpHeaders.ContentType, + ContentType.Text.EventStream.toString(), + ), + ) + } else { + respond("", HttpStatusCode.OK) + } + } + + val receivedMessages = mutableListOf() + val responseReceived = CompletableDeferred() + + transport.onMessage { message -> + receivedMessages.add(message) + if (message is JSONRPCResponse && !responseReceived.isCompleted) { + responseReceived.complete(Unit) + } + } + + transport.start() + + transport.send( + JSONRPCRequest( + id = "req-1", + method = "test", + params = buildJsonObject { }, + ), + ) + + eventually { + responseReceived.await() + } + + receivedMessages shouldHaveSize 1 + val response = receivedMessages[0] as JSONRPCResponse + response.id shouldBe RequestId.StringId("req-1") + + transport.close() + } + + @Test + fun testInlineSseHasPrimingEventTracking() = runTest { + val transport = createTransport { request -> + if (request.method == HttpMethod.Post) { + val sseContent = buildString { + // Event with id = priming event + appendLine("id: priming-1") + appendLine("event: message") + appendLine( + """data: {"jsonrpc":"2.0","method":"notifications/progress",""" + + """"params":{"progressToken":"t1","progress":50}}""", + ) + appendLine() + // Notification without id + appendLine("event: message") + appendLine("""data: {"jsonrpc":"2.0","method":"notifications/tools/list_changed"}""") + appendLine() + } + + respond( + content = ByteReadChannel(sseContent), + status = HttpStatusCode.OK, + headers = headersOf( + HttpHeaders.ContentType, + ContentType.Text.EventStream.toString(), + ), + ) + } else { + respond("", HttpStatusCode.OK) + } + } + + val receivedMessages = mutableListOf() + val twoMessagesReceived = CompletableDeferred() + + transport.onMessage { message -> + receivedMessages.add(message) + if (receivedMessages.size >= 2 && !twoMessagesReceived.isCompleted) { + twoMessagesReceived.complete(Unit) + } + } + + transport.start() + + transport.send( + JSONRPCRequest( + id = "test-1", + method = "test", + params = buildJsonObject { }, + ), + ) + + eventually { + twoMessagesReceived.await() + } + + receivedMessages shouldHaveSize 2 + // Both should be notifications (no JSONRPCResponse → POST-to-GET reconnect would be triggered) + receivedMessages[0].shouldBeInstanceOf() + receivedMessages[1].shouldBeInstanceOf() + + transport.close() + } + + @Test + fun testInlineSseResponseStopsReconnection() = runTest { + val transport = createTransport { request -> + if (request.method == HttpMethod.Post) { + val sseContent = buildString { + appendLine("id: ev-1") + appendLine("event: message") + appendLine("""data: {"jsonrpc":"2.0","id":"req-1","result":{"tools":[]}}""") + appendLine() + } + + respond( + content = ByteReadChannel(sseContent), + status = HttpStatusCode.OK, + headers = headersOf( + HttpHeaders.ContentType, + ContentType.Text.EventStream.toString(), + ), + ) + } else { + respond("", HttpStatusCode.OK) + } + } + + val receivedMessages = mutableListOf() + val responseReceived = CompletableDeferred() + + transport.onMessage { message -> + receivedMessages.add(message) + if (message is JSONRPCResponse && !responseReceived.isCompleted) { + responseReceived.complete(Unit) + } + } + + transport.start() + + transport.send( + JSONRPCRequest( + id = "req-1", + method = "tools/list", + params = buildJsonObject { }, + ), + ) + + eventually { + responseReceived.await() + } + + receivedMessages shouldHaveSize 1 + // Response received → no reconnection triggered (hasPrimingEvent=true, receivedResponse=true) + val response = receivedMessages[0] as JSONRPCResponse + response.id shouldBe RequestId.StringId("req-1") + + transport.close() + } + + @Test + fun testDeprecatedConstructorStillWorks() = runTest { + val mockEngine = MockEngine { _ -> + respond( + content = "", + status = HttpStatusCode.Accepted, + ) + } + val httpClient = HttpClient(mockEngine) { + install(SSE) + } + + val transport = + StreamableHttpClientTransport(httpClient, url = "http://localhost:8080/mcp", reconnectionTime = 2.seconds) + + transport.start() + transport.send(JSONRPCNotification(method = "test")) + transport.close() + } + + private suspend fun setupTransportAndCollectMessages( + transport: StreamableHttpClientTransport, + ): Pair, MutableList> { + val receivedMessages = mutableListOf() + val receivedErrors = mutableListOf() + + transport.onMessage { message -> + receivedMessages.add(message) + } + + transport.onError { error -> + receivedErrors.add(error) + } + + transport.start() + + transport.send( + JSONRPCRequest( + id = "test-close", + method = "test", + params = buildJsonObject { }, + ), + ) + + return receivedMessages to receivedErrors + } +} diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamingHttpClientTransportLifecycleTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamingHttpClientTransportLifecycleTest.kt new file mode 100644 index 0000000..f8ecec5 --- /dev/null +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamingHttpClientTransportLifecycleTest.kt @@ -0,0 +1,43 @@ +package io.modelcontextprotocol.kotlin.sdk.client.streamable.http + +import io.ktor.client.HttpClient +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.respond +import io.ktor.client.plugins.sse.SSE +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.headersOf +import io.modelcontextprotocol.kotlin.sdk.client.AbstractClientTransportLifecycleTest +import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.time.Duration.Companion.seconds + +class StreamingHttpClientTransportLifecycleTest : + AbstractClientTransportLifecycleTest() { + + /** + * Dummy method to make IDE treat this class as a test + */ + @Test + @Ignore + fun dummyTest() = Unit + + override fun createTransport(): StreamableHttpClientTransport { + val mockEngine = MockEngine { + respond( + "this is not valid json", + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()), + ) + } + val httpClient = HttpClient(mockEngine) { + install(SSE) { + reconnectionTime = 1.seconds + } + } + + return StreamableHttpClientTransport(httpClient, url = "http://localhost:8080/mcp") + } +} diff --git a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientSamplingValidationTest.kt b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientSamplingValidationTest.kt new file mode 100644 index 0000000..7775a08 --- /dev/null +++ b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientSamplingValidationTest.kt @@ -0,0 +1,98 @@ +package io.modelcontextprotocol.kotlin.sdk.client + +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import io.modelcontextprotocol.kotlin.sdk.types.Role +import io.modelcontextprotocol.kotlin.sdk.types.SamplingMessage +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.Tool +import io.modelcontextprotocol.kotlin.sdk.types.ToolChoice +import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema +import kotlinx.serialization.json.buildJsonObject +import org.junit.jupiter.api.assertDoesNotThrow +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class ClientSamplingValidationTest { + + private val dummyTool = Tool( + name = "t", + inputSchema = ToolSchema(properties = buildJsonObject { }, required = emptyList()), + ) + + private val minimalMessages = listOf(SamplingMessage(Role.User, TextContent("hi"))) + + private val noToolsCaps = ClientCapabilities(sampling = ClientCapabilities.sampling) + private val withToolsCaps = ClientCapabilities( + sampling = ClientCapabilities.Sampling(tools = EmptyJsonObject), + ) + + @Test + fun `request without tools or toolChoice is always accepted`() { + val request = CreateMessageRequest( + CreateMessageRequestParams(maxTokens = 10, messages = minimalMessages), + ) + assertDoesNotThrow { validateSamplingToolsCapability(request, noToolsCaps) } + } + + @Test + fun `tools without sampling tools capability throws InvalidParams`() { + val request = CreateMessageRequest( + CreateMessageRequestParams( + maxTokens = 10, + messages = minimalMessages, + tools = listOf(dummyTool), + ), + ) + val exception = assertFailsWith { + validateSamplingToolsCapability(request, noToolsCaps) + } + assertEquals(RPCError.ErrorCode.INVALID_PARAMS, exception.code) + check(exception.message?.contains("tools") == true) + } + + @Test + fun `toolChoice without sampling tools capability throws InvalidParams`() { + val request = CreateMessageRequest( + CreateMessageRequestParams( + maxTokens = 10, + messages = minimalMessages, + toolChoice = ToolChoice(mode = ToolChoice.Mode.Required), + ), + ) + val exception = assertFailsWith { + validateSamplingToolsCapability(request, noToolsCaps) + } + assertEquals(RPCError.ErrorCode.INVALID_PARAMS, exception.code) + check(exception.message?.contains("toolChoice") == true) + } + + @Test + fun `tools with sampling tools capability is accepted`() { + val request = CreateMessageRequest( + CreateMessageRequestParams( + maxTokens = 10, + messages = minimalMessages, + tools = listOf(dummyTool), + ), + ) + assertDoesNotThrow { validateSamplingToolsCapability(request, withToolsCaps) } + } + + @Test + fun `toolChoice with sampling tools capability is accepted`() { + val request = CreateMessageRequest( + CreateMessageRequestParams( + maxTokens = 10, + messages = minimalMessages, + toolChoice = ToolChoice(mode = ToolChoice.Mode.Auto), + ), + ) + assertDoesNotThrow { validateSamplingToolsCapability(request, withToolsCaps) } + } +} diff --git a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/stdio/StdioClientTransportErrorHandlingTest.kt b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/stdio/StdioClientTransportErrorHandlingTest.kt new file mode 100644 index 0000000..3c1915f --- /dev/null +++ b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/stdio/StdioClientTransportErrorHandlingTest.kt @@ -0,0 +1,216 @@ +package io.modelcontextprotocol.kotlin.sdk.client.stdio + +import io.kotest.assertions.nondeterministic.eventually +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.kotest.matchers.types.shouldBeSameInstanceAs +import io.mockk.coEvery +import io.mockk.mockk +import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ClosedSendChannelException +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import kotlinx.io.Buffer +import kotlinx.io.asSource +import kotlinx.io.buffered +import kotlinx.io.writeString +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.io.PipedInputStream +import java.io.PipedOutputStream +import java.util.stream.Stream +import kotlin.concurrent.atomics.AtomicBoolean +import kotlin.concurrent.atomics.ExperimentalAtomicApi +import kotlin.test.Test +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +/** + * Tests for StdioClientTransport error handling: EOF, IO errors, and edge cases. + */ +class StdioClientTransportErrorHandlingTest { + + private lateinit var transport: StdioClientTransport + + @OptIn(ExperimentalAtomicApi::class) + @Test + fun `should continue on stderr EOF`(): Unit = runBlocking(Dispatchers.IO) { + // Empty stderr = immediate EOF + val stderrBuffer = Buffer() + + // Create a pipe for stdin that stays open (simulates real stdin behavior) + val pipedOutputStream = PipedOutputStream() + val pipedInputStream = PipedInputStream(pipedOutputStream) + + // Write one message to stdin + pipedOutputStream.write("""data: {"jsonrpc":"2.0","method":"ping","id":1}\n\n""".toByteArray()) + pipedOutputStream.flush() + // Keep the pipe open by not closing pipedOutputStream - this prevents stdin EOF + + val outputBuffer = Buffer() + + transport = StdioClientTransport( + input = pipedInputStream.asSource().buffered(), + output = outputBuffer, + error = stderrBuffer, + ) + + val closeCalled = AtomicBoolean(false) + transport.onClose { closeCalled.store(true) } + + transport.start() + + // Wait for stderr EOF and stdin message to be processed + delay(500.milliseconds) + + // Transport should still be alive because stdin is still open (not EOF'd) + closeCalled.load() shouldBe false + + // Close pipes to trigger stdin EOF. + // `transport.close()` cann't help here, since the underlying Java read() is blocked on I/O operation + pipedOutputStream.close() + pipedInputStream.close() + + // Transport should close when stdin EOF is detected + eventually(2.seconds) { + closeCalled.load() shouldBe true + } + } + + @Test + fun `should call onClose exactly once on error scenarios`() = runTest { + val stderrBuffer = createNonEmptyBuffer { + "FATAL: critical error\n" + } + + val inputBuffer = Buffer() + val outputBuffer = Buffer() + + var closeCallCount = 0 + + transport = StdioClientTransport( + input = inputBuffer, + output = outputBuffer, + error = stderrBuffer, + classifyStderr = { StdioClientTransport.StderrSeverity.FATAL }, + ) + + transport.onClose { closeCallCount++ } + + transport.start() + + // FATAL stderr should trigger close, wait for it to complete + eventually(2.seconds) { + closeCallCount shouldBe 1 + } + + // Explicit close after error already closed it (should be no-op) + transport.close() + + closeCallCount shouldBe 1 + } + + @Test + fun `should handle empty input gracefully`() = runTest { + val inputBuffer = Buffer() + val outputBuffer = Buffer() + + transport = StdioClientTransport( + input = inputBuffer, + output = outputBuffer, + ) + + var errorCalled = false + transport.onError { errorCalled = true } + + transport.start() + + // Empty input should close cleanly without error + // Wait for EOF processing to complete, verify no error was called + eventually(2.seconds) { + errorCalled.shouldBeFalse() + } + } + + companion object { + @JvmStatic + fun exceptions(): Stream = Stream.of( + Arguments.of( + CancellationException(), + false, // should not wrap, propagate + null, + ), + Arguments.of( + McpException(-1, "dummy"), + false, // should not wrap, propagate + null, + ), + Arguments.of( + ClosedSendChannelException("dummy"), + true, // should wrap in McpException + ErrorCode.CONNECTION_CLOSED, + ), + Arguments.of( + Exception(), + true, + ErrorCode.INTERNAL_ERROR, + ), + ) + } + + @ParameterizedTest + @MethodSource("exceptions") + fun `Send should handle exceptions`(throwable: Exception, shouldWrap: Boolean, expectedCode: Int?) = runTest { + val sendChannel: Channel = mockk(relaxed = true) + + // Create stdin pipe that stays open to prevent transport from closing + val pipedOutputStream = PipedOutputStream() + val pipedInputStream = PipedInputStream(pipedOutputStream) + // Keep pipe open (don't write or close) - stdin will block on read, not EOF + + transport = StdioClientTransport( + input = pipedInputStream.asSource().buffered(), + output = Buffer(), + sendChannel = sendChannel, + ) + + coEvery { sendChannel.send(any()) } throws throwable + + transport.start() + + // Cancel the coroutine while it's suspended in send() + val exception = shouldThrow { + transport.send(JSONRPCRequest(id = "test-1", method = "test/method")) + } + + if (shouldWrap) { + exception.shouldBeInstanceOf { + it.cause shouldBeSameInstanceAs throwable + it.code shouldBe expectedCode + } + } else { + exception shouldBeSameInstanceAs throwable + } + + // Cleanup + pipedOutputStream.close() + pipedInputStream.close() + } + + fun createNonEmptyBuffer(block: () -> String): Buffer { + val buffer = Buffer() + buffer.writeString(block()) + return buffer + } +} diff --git a/kotlin-sdk-client/src/jvmTest/resources/junit-platform.properties b/kotlin-sdk-client/src/jvmTest/resources/junit-platform.properties new file mode 100644 index 0000000..9af7987 --- /dev/null +++ b/kotlin-sdk-client/src/jvmTest/resources/junit-platform.properties @@ -0,0 +1,6 @@ +## https://docs.junit.org/5.3.0-M1/user-guide/index.html#writing-tests-parallel-execution +junit.jupiter.execution.parallel.enabled=true +junit.jupiter.execution.parallel.config.strategy=dynamic +junit.jupiter.execution.parallel.mode.default=concurrent +junit.jupiter.execution.parallel.mode.classes.default=concurrent +junit.jupiter.execution.timeout.default=2m diff --git a/kotlin-sdk-client/src/jvmTest/resources/simplelogger.properties b/kotlin-sdk-client/src/jvmTest/resources/simplelogger.properties new file mode 100644 index 0000000..2336c13 --- /dev/null +++ b/kotlin-sdk-client/src/jvmTest/resources/simplelogger.properties @@ -0,0 +1,9 @@ +# Level of logging for the ROOT logger: ERROR, WARN, INFO, DEBUG, TRACE (default is INFO) +org.slf4j.simpleLogger.defaultLogLevel=INFO +org.slf4j.simpleLogger.showThreadName=true +org.slf4j.simpleLogger.showDateTime=false + +# Log level for specific packages or classes +org.slf4j.simpleLogger.log.io.ktor.server=DEBUG +org.slf4j.simpleLogger.log.io.modelcontextprotocol=DEBUG +org.slf4j.simpleLogger.log.dev.mokksy=DEBUG diff --git a/kotlin-sdk-core/Module.md b/kotlin-sdk-core/Module.md index 696360a..b4ed71b 100644 --- a/kotlin-sdk-core/Module.md +++ b/kotlin-sdk-core/Module.md @@ -1,5 +1,47 @@ # Module kotlin-sdk-core -The MCP Kotlin SDK Core module provides fundamental functionality for interacting with Model Context Protocol (MCP). -This module serves as the foundation for building MCP-enabled applications in Kotlin. +`kotlin-sdk-core` is the foundation of the MCP Kotlin SDK. It contains protocol message types, JSON handling, and +transport abstractions used by both client and server modules. No platform-specific code lives here; everything is +designed for Kotlin Multiplatform with explicit API mode enabled. +## What the module provides + +- **Protocol model**: Complete MCP data classes for requests, results, notifications, capabilities, tools, prompts, + resources, logging, completion, sampling, elicitation, and roots. DSL helpers (`*.dsl.kt`) let you build messages + fluently without repeating boilerplate. +- **JSON utilities**: A shared `McpJson` configuration (kotlinx.serialization) with MCP-friendly settings (ignore + unknown keys, no class discriminator). Helpers for converting maps to `JsonElement`, `EmptyJsonObject`, and + encoding/decoding JSON-RPC envelopes. +- **Transport abstractions**: `Transport` and `AbstractTransport` define the message pipeline, callbacks, and error + handling. `WebSocketMcpTransport` adds a shared WebSocket implementation for both client and server sides, and + `ReadBuffer` handles streaming JSON-RPC framing. +- **Protocol engine**: The `Protocol` base class manages request/response correlation, notifications, progress tokens, + and capability assertions. Higher-level modules extend it to become `Client` and `Server`. +- **Errors and safety**: Common exception types (`McpException`, parsing errors) plus capability enforcement hooks + ensure callers cannot use endpoints the peer does not advertise. + +## Typical usage + +- Import MCP types to define capabilities and message payloads for your own transports or custom integrations. +- Extend `Protocol` if you need a bespoke peer role while reusing correlation logic and JSON-RPC helpers. +- Use the DSL builders to construct well-typed requests (tools, prompts, resources, logging, completion) instead of + crafting raw JSON. + +### Example: building a tool call request + +```kotlin +val request = CallToolRequest { + name = "summarize" + arguments = mapOf("text" to "Hello MCP") +} +``` + +### Example: parsing a JSON-RPC message + +```kotlin +val message: JSONRPCMessage = McpJson.decodeFromString(jsonString) +``` + +Use this module when you need the raw building blocks of MCP—types, JSON config, and transport base classes—whether to +embed in another runtime, author new transports, or contribute higher-level features in the client/server modules. The +APIs are explicit to keep the shared surface stable for downstream users. diff --git a/kotlin-sdk-core/api/kotlin-sdk-core.api b/kotlin-sdk-core/api/kotlin-sdk-core.api index ed9a268..b3e88d8 100644 --- a/kotlin-sdk-core/api/kotlin-sdk-core.api +++ b/kotlin-sdk-core/api/kotlin-sdk-core.api @@ -1,2239 +1,4209 @@ -public final class io/modelcontextprotocol/kotlin/sdk/Annotations { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Annotations$Companion; - public fun (Ljava/util/List;Lkotlin/time/Instant;Ljava/lang/Double;)V - public final fun component1 ()Ljava/util/List; - public final fun component2 ()Lkotlin/time/Instant; - public final fun component3 ()Ljava/lang/Double; - public final fun copy (Ljava/util/List;Lkotlin/time/Instant;Ljava/lang/Double;)Lio/modelcontextprotocol/kotlin/sdk/Annotations; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/Annotations;Ljava/util/List;Lkotlin/time/Instant;Ljava/lang/Double;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/Annotations; - public fun equals (Ljava/lang/Object;)Z - public final fun getAudience ()Ljava/util/List; - public final fun getLastModified ()Lkotlin/time/Instant; - public final fun getPriority ()Ljava/lang/Double; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; +public abstract interface annotation class io/modelcontextprotocol/kotlin/sdk/ExperimentalMcpApi : java/lang/annotation/Annotation { } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/Annotations$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/Annotations$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/Annotations; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/Annotations;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/Annotations$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/AudioContent : io/modelcontextprotocol/kotlin/sdk/PromptMessageContentMultimodal { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/AudioContent$Companion; - public static final field TYPE Ljava/lang/String; - public fun (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Annotations;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Annotations;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/Annotations; - public final fun copy (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Annotations;)Lio/modelcontextprotocol/kotlin/sdk/AudioContent; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/AudioContent;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Annotations;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/AudioContent; - public fun equals (Ljava/lang/Object;)Z - public final fun getAnnotations ()Lio/modelcontextprotocol/kotlin/sdk/Annotations; - public final fun getData ()Ljava/lang/String; - public final fun getMimeType ()Ljava/lang/String; - public fun getType ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/AudioContent$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/AudioContent$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/AudioContent; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/AudioContent;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/AudioContent$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/BlobResourceContents : io/modelcontextprotocol/kotlin/sdk/ResourceContents { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/BlobResourceContents$Companion; - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/BlobResourceContents; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/BlobResourceContents;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/BlobResourceContents; - public fun equals (Ljava/lang/Object;)Z - public final fun getBlob ()Ljava/lang/String; - public fun getMimeType ()Ljava/lang/String; - public fun getUri ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/BlobResourceContents$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/BlobResourceContents$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/BlobResourceContents; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/BlobResourceContents;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/BlobResourceContents$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CallToolRequest : io/modelcontextprotocol/kotlin/sdk/ClientRequest, io/modelcontextprotocol/kotlin/sdk/WithMeta { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/CallToolRequest$Companion; - public fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Lkotlinx/serialization/json/JsonObject; - public final fun component3 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/CallToolRequest; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/CallToolRequest;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/CallToolRequest; - public fun equals (Ljava/lang/Object;)Z - public final fun getArguments ()Lkotlinx/serialization/json/JsonObject; - public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public final fun getName ()Ljava/lang/String; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/CallToolRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CallToolRequest$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/CallToolRequest; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/CallToolRequest;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CallToolRequest$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CallToolResult : io/modelcontextprotocol/kotlin/sdk/CallToolResultBase { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/CallToolResult$Companion; - public fun (Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun getContent ()Ljava/util/List; - public fun getStructuredContent ()Lkotlinx/serialization/json/JsonObject; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; - public fun isError ()Ljava/lang/Boolean; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/CallToolResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CallToolResult$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/CallToolResult; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/CallToolResult;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CallToolResult$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public abstract interface class io/modelcontextprotocol/kotlin/sdk/CallToolResultBase : io/modelcontextprotocol/kotlin/sdk/ServerResult { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/CallToolResultBase$Companion; - public abstract fun getContent ()Ljava/util/List; - public abstract fun getStructuredContent ()Lkotlinx/serialization/json/JsonObject; - public fun isError ()Ljava/lang/Boolean; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CallToolResultBase$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CallToolResultBase$DefaultImpls { - public static fun isError (Lio/modelcontextprotocol/kotlin/sdk/CallToolResultBase;)Ljava/lang/Boolean; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CancelledNotification : io/modelcontextprotocol/kotlin/sdk/ClientNotification, io/modelcontextprotocol/kotlin/sdk/ServerNotification { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Companion; - public fun (Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params;)Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification;Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification; - public fun equals (Ljava/lang/Object;)Z - public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params; - public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/CancelledNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CancelledNotification$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params : io/modelcontextprotocol/kotlin/sdk/NotificationParams { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params$Companion; - public fun (Lio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/RequestId; - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params;Lio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params; - public fun equals (Ljava/lang/Object;)Z - public final fun getReason ()Ljava/lang/String; - public final fun getRequestId ()Lio/modelcontextprotocol/kotlin/sdk/RequestId; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ClientCapabilities { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities$Companion; - public fun ()V - public fun (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lkotlinx/serialization/json/JsonObject; - public final fun component2 ()Lkotlinx/serialization/json/JsonObject; - public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities$Roots; - public final fun component4 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities; - public fun equals (Ljava/lang/Object;)Z - public final fun getElicitation ()Lkotlinx/serialization/json/JsonObject; - public final fun getExperimental ()Lkotlinx/serialization/json/JsonObject; - public final fun getRoots ()Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities$Roots; - public final fun getSampling ()Lkotlinx/serialization/json/JsonObject; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ClientCapabilities$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ClientCapabilities$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ClientCapabilities$Roots { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities$Roots$Companion; - public fun (Ljava/lang/Boolean;)V - public final fun component1 ()Ljava/lang/Boolean; - public final fun copy (Ljava/lang/Boolean;)Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities$Roots; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities$Roots;Ljava/lang/Boolean;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities$Roots; - public fun equals (Ljava/lang/Object;)Z - public final fun getListChanged ()Ljava/lang/Boolean; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ClientCapabilities$Roots$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities$Roots$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities$Roots; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities$Roots;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ClientCapabilities$Roots$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public abstract interface class io/modelcontextprotocol/kotlin/sdk/ClientNotification : io/modelcontextprotocol/kotlin/sdk/Notification { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ClientNotification$Companion; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ClientNotification$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public abstract interface class io/modelcontextprotocol/kotlin/sdk/ClientRequest : io/modelcontextprotocol/kotlin/sdk/Request { -} - -public abstract interface class io/modelcontextprotocol/kotlin/sdk/ClientResult : io/modelcontextprotocol/kotlin/sdk/RequestResult { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ClientResult$Companion; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ClientResult$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CompatibilityCallToolResult : io/modelcontextprotocol/kotlin/sdk/CallToolResultBase { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/CompatibilityCallToolResult$Companion; - public fun (Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun getContent ()Ljava/util/List; - public fun getStructuredContent ()Lkotlinx/serialization/json/JsonObject; - public final fun getToolResult ()Lkotlinx/serialization/json/JsonObject; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; - public fun isError ()Ljava/lang/Boolean; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/CompatibilityCallToolResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CompatibilityCallToolResult$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/CompatibilityCallToolResult; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/CompatibilityCallToolResult;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CompatibilityCallToolResult$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CompleteRequest : io/modelcontextprotocol/kotlin/sdk/ClientRequest, io/modelcontextprotocol/kotlin/sdk/WithMeta { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest$Companion; - public fun (Lio/modelcontextprotocol/kotlin/sdk/Reference;Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest$Argument;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/Reference;Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest$Argument;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/Reference; - public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest$Argument; - public final fun component3 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/Reference;Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest$Argument;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest;Lio/modelcontextprotocol/kotlin/sdk/Reference;Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest$Argument;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest; - public fun equals (Ljava/lang/Object;)Z - public final fun getArgument ()Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest$Argument; - public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public final fun getRef ()Lio/modelcontextprotocol/kotlin/sdk/Reference; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/CompleteRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CompleteRequest$Argument { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest$Argument$Companion; - public fun (Ljava/lang/String;Ljava/lang/String;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest$Argument; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest$Argument;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest$Argument; - public fun equals (Ljava/lang/Object;)Z - public final fun getName ()Ljava/lang/String; - public final fun getValue ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/CompleteRequest$Argument$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest$Argument$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest$Argument; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest$Argument;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CompleteRequest$Argument$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CompleteRequest$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CompleteResult : io/modelcontextprotocol/kotlin/sdk/ServerResult { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/CompleteResult$Companion; - public fun (Lio/modelcontextprotocol/kotlin/sdk/CompleteResult$Completion;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/CompleteResult$Completion;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/CompleteResult$Completion; - public final fun component2 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/CompleteResult$Completion;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/CompleteResult; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/CompleteResult;Lio/modelcontextprotocol/kotlin/sdk/CompleteResult$Completion;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/CompleteResult; - public fun equals (Ljava/lang/Object;)Z - public final fun getCompletion ()Lio/modelcontextprotocol/kotlin/sdk/CompleteResult$Completion; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/CompleteResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CompleteResult$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/CompleteResult; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/CompleteResult;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CompleteResult$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CompleteResult$Completion { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/CompleteResult$Completion$Companion; - public fun (Ljava/util/List;Ljava/lang/Integer;Ljava/lang/Boolean;)V - public final fun getHasMore ()Ljava/lang/Boolean; - public final fun getTotal ()Ljava/lang/Integer; - public final fun getValues ()Ljava/util/List; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/CompleteResult$Completion$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CompleteResult$Completion$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/CompleteResult$Completion; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/CompleteResult$Completion;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CompleteResult$Completion$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest : io/modelcontextprotocol/kotlin/sdk/ServerRequest, io/modelcontextprotocol/kotlin/sdk/WithMeta { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$Companion; - public fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$RequestedSchema;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$RequestedSchema;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$RequestedSchema; - public final fun component3 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$RequestedSchema;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$RequestedSchema;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest; - public fun equals (Ljava/lang/Object;)Z - public final fun getMessage ()Ljava/lang/String; - public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public final fun getRequestedSchema ()Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$RequestedSchema; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$RequestedSchema { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$RequestedSchema$Companion; - public fun ()V - public fun (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;)V - public synthetic fun (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lkotlinx/serialization/json/JsonObject; - public final fun component2 ()Ljava/util/List; - public final fun copy (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;)Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$RequestedSchema; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$RequestedSchema;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$RequestedSchema; - public fun equals (Ljava/lang/Object;)Z - public final fun getProperties ()Lkotlinx/serialization/json/JsonObject; - public final fun getRequired ()Ljava/util/List; - public final fun getType ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$RequestedSchema$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$RequestedSchema$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$RequestedSchema; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$RequestedSchema;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$RequestedSchema$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CreateElicitationResult : io/modelcontextprotocol/kotlin/sdk/ClientResult { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationResult$Companion; - public fun (Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationResult$Action;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationResult$Action;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationResult$Action; - public final fun component2 ()Lkotlinx/serialization/json/JsonObject; - public final fun component3 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationResult$Action;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationResult; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationResult;Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationResult$Action;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationResult; - public fun equals (Ljava/lang/Object;)Z - public final fun getAction ()Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationResult$Action; - public final fun getContent ()Lkotlinx/serialization/json/JsonObject; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/CreateElicitationResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationResult$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationResult; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationResult;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CreateElicitationResult$Action : java/lang/Enum { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationResult$Action$Companion; - public static final field accept Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationResult$Action; - public static final field cancel Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationResult$Action; - public static final field decline Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationResult$Action; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationResult$Action; - public static fun values ()[Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationResult$Action; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CreateElicitationResult$Action$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CreateElicitationResult$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CreateMessageRequest : io/modelcontextprotocol/kotlin/sdk/ServerRequest, io/modelcontextprotocol/kotlin/sdk/WithMeta { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest$Companion; - public fun (Ljava/util/List;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest$IncludeContext;Ljava/lang/Double;ILjava/util/List;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/ModelPreferences;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Ljava/util/List;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest$IncludeContext;Ljava/lang/Double;ILjava/util/List;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/ModelPreferences;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/util/List; - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest$IncludeContext; - public final fun component4 ()Ljava/lang/Double; - public final fun component5 ()I - public final fun component6 ()Ljava/util/List; - public final fun component7 ()Lkotlinx/serialization/json/JsonObject; - public final fun component8 ()Lio/modelcontextprotocol/kotlin/sdk/ModelPreferences; - public final fun component9 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Ljava/util/List;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest$IncludeContext;Ljava/lang/Double;ILjava/util/List;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/ModelPreferences;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest;Ljava/util/List;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest$IncludeContext;Ljava/lang/Double;ILjava/util/List;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/ModelPreferences;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest; - public fun equals (Ljava/lang/Object;)Z - public final fun getIncludeContext ()Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest$IncludeContext; - public final fun getMaxTokens ()I - public final fun getMessages ()Ljava/util/List; - public final fun getMetadata ()Lkotlinx/serialization/json/JsonObject; - public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public final fun getModelPreferences ()Lio/modelcontextprotocol/kotlin/sdk/ModelPreferences; - public final fun getStopSequences ()Ljava/util/List; - public final fun getSystemPrompt ()Ljava/lang/String; - public final fun getTemperature ()Ljava/lang/Double; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/CreateMessageRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CreateMessageRequest$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CreateMessageRequest$IncludeContext : java/lang/Enum { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest$IncludeContext$Companion; - public static final field allServers Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest$IncludeContext; - public static final field none Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest$IncludeContext; - public static final field thisServer Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest$IncludeContext; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest$IncludeContext; - public static fun values ()[Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest$IncludeContext; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CreateMessageRequest$IncludeContext$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CreateMessageResult : io/modelcontextprotocol/kotlin/sdk/ClientResult { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/CreateMessageResult$Companion; - public fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/StopReason;Lio/modelcontextprotocol/kotlin/sdk/Role;Lio/modelcontextprotocol/kotlin/sdk/PromptMessageContentMultimodal;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/StopReason;Lio/modelcontextprotocol/kotlin/sdk/Role;Lio/modelcontextprotocol/kotlin/sdk/PromptMessageContentMultimodal;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/StopReason; - public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/Role; - public final fun component4 ()Lio/modelcontextprotocol/kotlin/sdk/PromptMessageContentMultimodal; - public final fun component5 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/StopReason;Lio/modelcontextprotocol/kotlin/sdk/Role;Lio/modelcontextprotocol/kotlin/sdk/PromptMessageContentMultimodal;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/CreateMessageResult; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/CreateMessageResult;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/StopReason;Lio/modelcontextprotocol/kotlin/sdk/Role;Lio/modelcontextprotocol/kotlin/sdk/PromptMessageContentMultimodal;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/CreateMessageResult; - public fun equals (Ljava/lang/Object;)Z - public final fun getContent ()Lio/modelcontextprotocol/kotlin/sdk/PromptMessageContentMultimodal; - public final fun getModel ()Ljava/lang/String; - public final fun getRole ()Lio/modelcontextprotocol/kotlin/sdk/Role; - public final fun getStopReason ()Lio/modelcontextprotocol/kotlin/sdk/StopReason; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/CreateMessageResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CreateMessageResult$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/CreateMessageResult; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/CreateMessageResult;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CreateMessageResult$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CustomMeta : io/modelcontextprotocol/kotlin/sdk/WithMeta { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/CustomMeta$Companion; - public fun ()V - public fun (Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/CustomMeta$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CustomMeta$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/CustomMeta; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/CustomMeta;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CustomMeta$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public class io/modelcontextprotocol/kotlin/sdk/CustomRequest : io/modelcontextprotocol/kotlin/sdk/Request { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/CustomRequest$Companion; - public synthetic fun (ILio/modelcontextprotocol/kotlin/sdk/Method;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V - public fun (Lio/modelcontextprotocol/kotlin/sdk/Method;)V - public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public static final synthetic fun write$Self (Lio/modelcontextprotocol/kotlin/sdk/CustomRequest;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/CustomRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CustomRequest$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/CustomRequest; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/CustomRequest;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CustomRequest$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/EmbeddedResource : io/modelcontextprotocol/kotlin/sdk/PromptMessageContent { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/EmbeddedResource$Companion; - public static final field TYPE Ljava/lang/String; - public fun (Lio/modelcontextprotocol/kotlin/sdk/ResourceContents;Lio/modelcontextprotocol/kotlin/sdk/Annotations;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/ResourceContents;Lio/modelcontextprotocol/kotlin/sdk/Annotations;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/ResourceContents; - public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/Annotations; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/ResourceContents;Lio/modelcontextprotocol/kotlin/sdk/Annotations;)Lio/modelcontextprotocol/kotlin/sdk/EmbeddedResource; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/EmbeddedResource;Lio/modelcontextprotocol/kotlin/sdk/ResourceContents;Lio/modelcontextprotocol/kotlin/sdk/Annotations;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/EmbeddedResource; - public fun equals (Ljava/lang/Object;)Z - public final fun getAnnotations ()Lio/modelcontextprotocol/kotlin/sdk/Annotations; - public final fun getResource ()Lio/modelcontextprotocol/kotlin/sdk/ResourceContents; - public fun getType ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/EmbeddedResource$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/EmbeddedResource$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/EmbeddedResource; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/EmbeddedResource;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/EmbeddedResource$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/EmptyRequestResult : io/modelcontextprotocol/kotlin/sdk/ClientResult, io/modelcontextprotocol/kotlin/sdk/ServerResult { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/EmptyRequestResult$Companion; - public fun ()V - public fun (Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/EmptyRequestResult; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/EmptyRequestResult;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/EmptyRequestResult; - public fun equals (Ljava/lang/Object;)Z - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/EmptyRequestResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/EmptyRequestResult$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/EmptyRequestResult; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/EmptyRequestResult;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/EmptyRequestResult$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public abstract interface class io/modelcontextprotocol/kotlin/sdk/ErrorCode { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ErrorCode$Companion; - public abstract fun getCode ()I -} - -public final class io/modelcontextprotocol/kotlin/sdk/ErrorCode$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ErrorCode$Defined : java/lang/Enum, io/modelcontextprotocol/kotlin/sdk/ErrorCode { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ErrorCode$Defined$Companion; - public static final field ConnectionClosed Lio/modelcontextprotocol/kotlin/sdk/ErrorCode$Defined; - public static final field InternalError Lio/modelcontextprotocol/kotlin/sdk/ErrorCode$Defined; - public static final field InvalidParams Lio/modelcontextprotocol/kotlin/sdk/ErrorCode$Defined; - public static final field InvalidRequest Lio/modelcontextprotocol/kotlin/sdk/ErrorCode$Defined; - public static final field MethodNotFound Lio/modelcontextprotocol/kotlin/sdk/ErrorCode$Defined; - public static final field ParseError Lio/modelcontextprotocol/kotlin/sdk/ErrorCode$Defined; - public static final field RequestTimeout Lio/modelcontextprotocol/kotlin/sdk/ErrorCode$Defined; - public fun getCode ()I - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/ErrorCode$Defined; - public static fun values ()[Lio/modelcontextprotocol/kotlin/sdk/ErrorCode$Defined; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ErrorCode$Defined$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ErrorCode$Unknown : io/modelcontextprotocol/kotlin/sdk/ErrorCode { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ErrorCode$Unknown$Companion; - public fun (I)V - public final fun component1 ()I - public final fun copy (I)Lio/modelcontextprotocol/kotlin/sdk/ErrorCode$Unknown; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ErrorCode$Unknown;IILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ErrorCode$Unknown; - public fun equals (Ljava/lang/Object;)Z - public fun getCode ()I - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ErrorCode$Unknown$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ErrorCode$Unknown$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ErrorCode$Unknown; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ErrorCode$Unknown;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ErrorCode$Unknown$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/GetPromptRequest : io/modelcontextprotocol/kotlin/sdk/ClientRequest, io/modelcontextprotocol/kotlin/sdk/WithMeta { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/GetPromptRequest$Companion; - public fun (Ljava/lang/String;Ljava/util/Map;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Ljava/lang/String;Ljava/util/Map;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/util/Map; - public final fun component3 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Ljava/lang/String;Ljava/util/Map;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/GetPromptRequest; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/GetPromptRequest;Ljava/lang/String;Ljava/util/Map;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/GetPromptRequest; - public fun equals (Ljava/lang/Object;)Z - public final fun getArguments ()Ljava/util/Map; - public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public final fun getName ()Ljava/lang/String; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/GetPromptRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/GetPromptRequest$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/GetPromptRequest; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/GetPromptRequest;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/GetPromptRequest$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/GetPromptResult : io/modelcontextprotocol/kotlin/sdk/ServerResult { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/GetPromptResult$Companion; - public fun (Ljava/lang/String;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Ljava/lang/String;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getDescription ()Ljava/lang/String; - public final fun getMessages ()Ljava/util/List; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/GetPromptResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/GetPromptResult$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/GetPromptResult; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/GetPromptResult;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/GetPromptResult$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ImageContent : io/modelcontextprotocol/kotlin/sdk/PromptMessageContentMultimodal { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ImageContent$Companion; - public static final field TYPE Ljava/lang/String; - public fun (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Annotations;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Annotations;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/Annotations; - public final fun copy (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Annotations;)Lio/modelcontextprotocol/kotlin/sdk/ImageContent; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ImageContent;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Annotations;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ImageContent; - public fun equals (Ljava/lang/Object;)Z - public final fun getAnnotations ()Lio/modelcontextprotocol/kotlin/sdk/Annotations; - public final fun getData ()Ljava/lang/String; - public final fun getMimeType ()Ljava/lang/String; - public fun getType ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ImageContent$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ImageContent$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ImageContent; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ImageContent;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ImageContent$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/Implementation { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Implementation$Companion; - public fun (Ljava/lang/String;Ljava/lang/String;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/Implementation; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/Implementation; - public fun equals (Ljava/lang/Object;)Z - public final fun getName ()Ljava/lang/String; - public final fun getVersion ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/Implementation$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/Implementation$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/Implementation; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/Implementation;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/Implementation$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/InitializeRequest : io/modelcontextprotocol/kotlin/sdk/ClientRequest, io/modelcontextprotocol/kotlin/sdk/WithMeta { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/InitializeRequest$Companion; - public fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities;Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities;Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities; - public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/Implementation; - public final fun component4 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities;Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/InitializeRequest; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/InitializeRequest;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities;Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/InitializeRequest; - public fun equals (Ljava/lang/Object;)Z - public final fun getCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities; - public final fun getClientInfo ()Lio/modelcontextprotocol/kotlin/sdk/Implementation; - public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public final fun getProtocolVersion ()Ljava/lang/String; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/InitializeRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/InitializeRequest$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/InitializeRequest; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/InitializeRequest;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/InitializeRequest$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/InitializeResult : io/modelcontextprotocol/kotlin/sdk/ServerResult { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/InitializeResult$Companion; - public fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities; - public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/Implementation; - public final fun component4 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/InitializeResult; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/InitializeResult;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/InitializeResult; - public fun equals (Ljava/lang/Object;)Z - public final fun getCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities; - public final fun getProtocolVersion ()Ljava/lang/String; - public final fun getServerInfo ()Lio/modelcontextprotocol/kotlin/sdk/Implementation; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/InitializeResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/InitializeResult$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/InitializeResult; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/InitializeResult;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/InitializeResult$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/InitializedNotification : io/modelcontextprotocol/kotlin/sdk/ClientNotification { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Companion; - public fun ()V - public fun (Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params;)Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification;Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification; - public fun equals (Ljava/lang/Object;)Z - public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params; - public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/InitializedNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/InitializedNotification$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params : io/modelcontextprotocol/kotlin/sdk/NotificationParams { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params$Companion; - public fun ()V - public fun (Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params; - public fun equals (Ljava/lang/Object;)Z - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/JSONRPCError : io/modelcontextprotocol/kotlin/sdk/JSONRPCMessage { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/JSONRPCError$Companion; - public fun (Lio/modelcontextprotocol/kotlin/sdk/ErrorCode;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/ErrorCode;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/ErrorCode; - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/ErrorCode;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/JSONRPCError; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCError;Lio/modelcontextprotocol/kotlin/sdk/ErrorCode;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/JSONRPCError; - public fun equals (Ljava/lang/Object;)Z - public final fun getCode ()Lio/modelcontextprotocol/kotlin/sdk/ErrorCode; - public final fun getData ()Lkotlinx/serialization/json/JsonObject; - public final fun getMessage ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/JSONRPCError$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/JSONRPCError$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/JSONRPCError; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/JSONRPCError;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/JSONRPCError$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public abstract interface class io/modelcontextprotocol/kotlin/sdk/JSONRPCMessage { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage$Companion; -} - -public final class io/modelcontextprotocol/kotlin/sdk/JSONRPCMessage$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/JSONRPCNotification : io/modelcontextprotocol/kotlin/sdk/JSONRPCMessage { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/JSONRPCNotification$Companion; - public fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Lkotlinx/serialization/json/JsonElement; - public final fun component3 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/JSONRPCNotification; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCNotification;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/JSONRPCNotification; - public fun equals (Ljava/lang/Object;)Z - public final fun getJsonrpc ()Ljava/lang/String; - public final fun getMethod ()Ljava/lang/String; - public final fun getParams ()Lkotlinx/serialization/json/JsonElement; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/JSONRPCNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/JSONRPCNotification$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/JSONRPCNotification; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/JSONRPCNotification;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/JSONRPCNotification$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/JSONRPCRequest : io/modelcontextprotocol/kotlin/sdk/JSONRPCMessage { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/JSONRPCRequest$Companion; - public fun (Lio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/RequestId; - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Lkotlinx/serialization/json/JsonElement; - public final fun component4 ()Ljava/lang/String; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/JSONRPCRequest; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCRequest;Lio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/JSONRPCRequest; - public fun equals (Ljava/lang/Object;)Z - public final fun getId ()Lio/modelcontextprotocol/kotlin/sdk/RequestId; - public final fun getJsonrpc ()Ljava/lang/String; - public final fun getMethod ()Ljava/lang/String; - public final fun getParams ()Lkotlinx/serialization/json/JsonElement; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/JSONRPCRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/JSONRPCRequest$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/JSONRPCRequest; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/JSONRPCRequest;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/JSONRPCRequest$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/JSONRPCResponse : io/modelcontextprotocol/kotlin/sdk/JSONRPCMessage { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/JSONRPCResponse$Companion; - public fun (Lio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/RequestResult;Lio/modelcontextprotocol/kotlin/sdk/JSONRPCError;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/RequestResult;Lio/modelcontextprotocol/kotlin/sdk/JSONRPCError;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/RequestResult;Lio/modelcontextprotocol/kotlin/sdk/JSONRPCError;)Lio/modelcontextprotocol/kotlin/sdk/JSONRPCResponse; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCResponse;Lio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/RequestResult;Lio/modelcontextprotocol/kotlin/sdk/JSONRPCError;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/JSONRPCResponse; - public final fun getError ()Lio/modelcontextprotocol/kotlin/sdk/JSONRPCError; - public final fun getId ()Lio/modelcontextprotocol/kotlin/sdk/RequestId; - public final fun getJsonrpc ()Ljava/lang/String; - public final fun getResult ()Lio/modelcontextprotocol/kotlin/sdk/RequestResult; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/JSONRPCResponse$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/JSONRPCResponse$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/JSONRPCResponse; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/JSONRPCResponse;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/JSONRPCResponse$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; +public abstract interface annotation class io/modelcontextprotocol/kotlin/sdk/InternalMcpApi : java/lang/annotation/Annotation { } public final class io/modelcontextprotocol/kotlin/sdk/LibVersionKt { public static final field LIB_VERSION Ljava/lang/String; } -public final class io/modelcontextprotocol/kotlin/sdk/ListPromptsRequest : io/modelcontextprotocol/kotlin/sdk/ClientRequest, io/modelcontextprotocol/kotlin/sdk/PaginatedRequest { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ListPromptsRequest$Companion; +public final class io/modelcontextprotocol/kotlin/sdk/internal/Utils_jvmKt { + public static final fun getIODispatcher ()Lkotlinx/coroutines/CoroutineDispatcher; +} + +public abstract class io/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { public fun ()V - public fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/ListPromptsRequest; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ListPromptsRequest;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ListPromptsRequest; - public fun equals (Ljava/lang/Object;)Z - public fun getCursor ()Ljava/lang/String; - public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; + protected final fun checkState (Lio/modelcontextprotocol/kotlin/sdk/shared/ClientTransportState;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun checkState$default (Lio/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransport;Lio/modelcontextprotocol/kotlin/sdk/shared/ClientTransportState;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + protected abstract fun closeResources (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + protected abstract fun getLogger ()Lio/github/oshai/kotlinlogging/KLogger; + protected final fun getState ()Lio/modelcontextprotocol/kotlin/sdk/shared/ClientTransportState; + protected abstract fun initialize (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + protected abstract fun performSend (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Lio/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun performSend$default (Lio/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransport;Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Lio/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public fun send (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Lio/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + protected final fun stateTransition (Lio/modelcontextprotocol/kotlin/sdk/shared/ClientTransportState;Lio/modelcontextprotocol/kotlin/sdk/shared/ClientTransportState;)V + protected final fun updateState (Lio/modelcontextprotocol/kotlin/sdk/shared/ClientTransportState;)V } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ListPromptsRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ListPromptsRequest$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ListPromptsRequest; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ListPromptsRequest;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ListPromptsRequest$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ListPromptsResult : io/modelcontextprotocol/kotlin/sdk/PaginatedResult, io/modelcontextprotocol/kotlin/sdk/ServerResult { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ListPromptsResult$Companion; - public fun (Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun getNextCursor ()Ljava/lang/String; - public final fun getPrompts ()Ljava/util/List; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ListPromptsResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ListPromptsResult$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ListPromptsResult; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ListPromptsResult;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ListPromptsResult$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ListResourceTemplatesRequest : io/modelcontextprotocol/kotlin/sdk/ClientRequest, io/modelcontextprotocol/kotlin/sdk/PaginatedRequest { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ListResourceTemplatesRequest$Companion; - public fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/ListResourceTemplatesRequest; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ListResourceTemplatesRequest;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ListResourceTemplatesRequest; - public fun equals (Ljava/lang/Object;)Z - public fun getCursor ()Ljava/lang/String; - public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ListResourceTemplatesRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ListResourceTemplatesRequest$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ListResourceTemplatesRequest; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ListResourceTemplatesRequest;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ListResourceTemplatesRequest$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ListResourceTemplatesResult : io/modelcontextprotocol/kotlin/sdk/PaginatedResult, io/modelcontextprotocol/kotlin/sdk/ServerResult { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ListResourceTemplatesResult$Companion; - public fun (Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun getNextCursor ()Ljava/lang/String; - public final fun getResourceTemplates ()Ljava/util/List; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ListResourceTemplatesResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ListResourceTemplatesResult$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ListResourceTemplatesResult; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ListResourceTemplatesResult;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ListResourceTemplatesResult$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ListResourcesRequest : io/modelcontextprotocol/kotlin/sdk/ClientRequest, io/modelcontextprotocol/kotlin/sdk/PaginatedRequest { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ListResourcesRequest$Companion; +public abstract class io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport : io/modelcontextprotocol/kotlin/sdk/shared/Transport { public fun ()V - public fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/ListResourcesRequest; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ListResourcesRequest;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ListResourcesRequest; - public fun equals (Ljava/lang/Object;)Z - public fun getCursor ()Ljava/lang/String; - public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; + protected final fun get_onError ()Lkotlin/jvm/functions/Function1; + protected final fun get_onMessage ()Lkotlin/jvm/functions/Function2; + protected final fun invokeOnCloseCallback ()V + public fun onClose (Lkotlin/jvm/functions/Function0;)V + public fun onError (Lkotlin/jvm/functions/Function1;)V + public fun onMessage (Lkotlin/jvm/functions/Function2;)V } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ListResourcesRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ListResourcesRequest$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ListResourcesRequest; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ListResourcesRequest;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ListResourcesRequest$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ListResourcesResult : io/modelcontextprotocol/kotlin/sdk/PaginatedResult, io/modelcontextprotocol/kotlin/sdk/ServerResult { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ListResourcesResult$Companion; - public fun (Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun getNextCursor ()Ljava/lang/String; - public final fun getResources ()Ljava/util/List; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ListResourcesResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ListResourcesResult$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ListResourcesResult; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ListResourcesResult;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ListResourcesResult$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ListRootsRequest : io/modelcontextprotocol/kotlin/sdk/ServerRequest, io/modelcontextprotocol/kotlin/sdk/WithMeta { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ListRootsRequest$Companion; - public fun ()V - public fun (Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ListRootsRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ListRootsRequest$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ListRootsRequest; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ListRootsRequest;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ListRootsRequest$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ListRootsResult : io/modelcontextprotocol/kotlin/sdk/ClientResult { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ListRootsResult$Companion; - public fun (Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getRoots ()Ljava/util/List; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ListRootsResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ListRootsResult$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ListRootsResult; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ListRootsResult;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ListRootsResult$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ListToolsRequest : io/modelcontextprotocol/kotlin/sdk/ClientRequest, io/modelcontextprotocol/kotlin/sdk/PaginatedRequest { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ListToolsRequest$Companion; - public fun ()V - public fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/ListToolsRequest; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ListToolsRequest;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ListToolsRequest; - public fun equals (Ljava/lang/Object;)Z - public fun getCursor ()Ljava/lang/String; - public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ListToolsRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ListToolsRequest$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ListToolsRequest; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ListToolsRequest;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ListToolsRequest$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ListToolsResult : io/modelcontextprotocol/kotlin/sdk/PaginatedResult, io/modelcontextprotocol/kotlin/sdk/ServerResult { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ListToolsResult$Companion; - public fun (Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun getNextCursor ()Ljava/lang/String; - public final fun getTools ()Ljava/util/List; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ListToolsResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ListToolsResult$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ListToolsResult; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ListToolsResult;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ListToolsResult$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/LoggingLevel : java/lang/Enum { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel$Companion; - public static final field alert Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel; - public static final field critical Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel; - public static final field debug Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel; - public static final field emergency Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel; - public static final field error Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel; - public static final field info Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel; - public static final field notice Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel; - public static final field warning Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel; +public final class io/modelcontextprotocol/kotlin/sdk/shared/ClientTransportState : java/lang/Enum { + public static final field InitializationFailed Lio/modelcontextprotocol/kotlin/sdk/shared/ClientTransportState; + public static final field Initializing Lio/modelcontextprotocol/kotlin/sdk/shared/ClientTransportState; + public static final field New Lio/modelcontextprotocol/kotlin/sdk/shared/ClientTransportState; + public static final field Operational Lio/modelcontextprotocol/kotlin/sdk/shared/ClientTransportState; + public static final field ShutdownFailed Lio/modelcontextprotocol/kotlin/sdk/shared/ClientTransportState; + public static final field ShuttingDown Lio/modelcontextprotocol/kotlin/sdk/shared/ClientTransportState; + public static final field Stopped Lio/modelcontextprotocol/kotlin/sdk/shared/ClientTransportState; public static fun getEntries ()Lkotlin/enums/EnumEntries; - public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel; - public static fun values ()[Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel; + public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/shared/ClientTransportState; + public static fun values ()[Lio/modelcontextprotocol/kotlin/sdk/shared/ClientTransportState; } -public final class io/modelcontextprotocol/kotlin/sdk/LoggingLevel$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; +public abstract class io/modelcontextprotocol/kotlin/sdk/shared/Protocol { + public fun (Lio/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptions;)V + protected abstract fun assertCapabilityForMethod (Lio/modelcontextprotocol/kotlin/sdk/types/Method;)V + protected abstract fun assertNotificationCapability (Lio/modelcontextprotocol/kotlin/sdk/types/Method;)V + public abstract fun assertRequestHandlerCapability (Lio/modelcontextprotocol/kotlin/sdk/types/Method;)V + public final fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun connect (Lio/modelcontextprotocol/kotlin/sdk/shared/Transport;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + protected final fun enableConcurrentDispatch ()V + public final fun getFallbackNotificationHandler ()Lkotlin/jvm/functions/Function2; + public final fun getFallbackRequestHandler ()Lkotlin/jvm/functions/Function3; + public final fun getNotificationHandlers ()Ljava/util/Map; + public final fun getOptions ()Lio/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptions; + public final fun getProgressHandlers ()Ljava/util/Map; + public final fun getRequestHandlers ()Ljava/util/Map; + public final fun getResponseHandlers ()Ljava/util/Map; + public final fun getTransport ()Lio/modelcontextprotocol/kotlin/sdk/shared/Transport; + public final fun notification (Lio/modelcontextprotocol/kotlin/sdk/types/Notification;Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun notification$default (Lio/modelcontextprotocol/kotlin/sdk/shared/Protocol;Lio/modelcontextprotocol/kotlin/sdk/types/Notification;Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public fun onClose ()V + public fun onError (Ljava/lang/Throwable;)V + protected fun onInitializedNotification ()V + public final fun removeNotificationHandler (Lio/modelcontextprotocol/kotlin/sdk/types/Method;)V + public final fun removeRequestHandler (Lio/modelcontextprotocol/kotlin/sdk/types/Method;)V + public final fun request (Lio/modelcontextprotocol/kotlin/sdk/types/Request;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun request$default (Lio/modelcontextprotocol/kotlin/sdk/shared/Protocol;Lio/modelcontextprotocol/kotlin/sdk/types/Request;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun setFallbackNotificationHandler (Lkotlin/jvm/functions/Function2;)V + public final fun setFallbackRequestHandler (Lkotlin/jvm/functions/Function3;)V + public final fun setNotificationHandler (Lio/modelcontextprotocol/kotlin/sdk/types/Method;Lkotlin/jvm/functions/Function1;)V + public final fun setRequestHandlerInternal (Lio/modelcontextprotocol/kotlin/sdk/types/Method;Lkotlin/jvm/functions/Function3;)V + protected fun wrapRequestHandler (Lio/modelcontextprotocol/kotlin/sdk/types/Method;Lkotlin/jvm/functions/Function3;)Lkotlin/jvm/functions/Function3; } -public final class io/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification : io/modelcontextprotocol/kotlin/sdk/ServerNotification { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Companion; - public fun (Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params;)Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification;Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification; +public final class io/modelcontextprotocol/kotlin/sdk/shared/ProtocolKt { + public static final field IMPLEMENTATION_NAME Ljava/lang/String; + public static final fun currentRequestHandlerExtra (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun getDEFAULT_REQUEST_TIMEOUT ()J +} + +public class io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptions { + public synthetic fun (ZJLkotlin/coroutines/CoroutineContext;IIILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (ZJLkotlin/coroutines/CoroutineContext;IILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getEnforceStrictCapabilities ()Z + public final fun getHandlerCoroutineContext ()Lkotlin/coroutines/CoroutineContext; + public final fun getTimeout-UwyO8pc ()J + public final fun setEnforceStrictCapabilities (Z)V + public final fun setHandlerCoroutineContext (Lkotlin/coroutines/CoroutineContext;)V + public final fun setTimeout-LRDsOJo (J)V +} + +public final class io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer { + public fun ()V + public final fun append ([B)V + public final fun clear ()V + public final fun readMessage ()Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage; +} + +public final class io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferKt { + public static final fun serializeMessage (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;)Ljava/lang/String; +} + +public final class io/modelcontextprotocol/kotlin/sdk/shared/RequestHandlerExtra : kotlin/coroutines/AbstractCoroutineContextElement { + public static final field Key Lio/modelcontextprotocol/kotlin/sdk/shared/RequestHandlerExtra$Key; + public final fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public final fun getRequestId ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestId; + public final fun sendNotification (Lio/modelcontextprotocol/kotlin/sdk/types/Notification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun sendRequest (Lio/modelcontextprotocol/kotlin/sdk/types/Request;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun sendRequest$default (Lio/modelcontextprotocol/kotlin/sdk/shared/RequestHandlerExtra;Lio/modelcontextprotocol/kotlin/sdk/types/Request;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; +} + +public final class io/modelcontextprotocol/kotlin/sdk/shared/RequestHandlerExtra$Key : kotlin/coroutines/CoroutineContext$Key { +} + +public final class io/modelcontextprotocol/kotlin/sdk/shared/RequestOptions : io/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions { + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;JILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component4 ()Lkotlin/jvm/functions/Function1; + public final fun component5-UwyO8pc ()J + public final fun copy-9VgGkz4 (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;J)Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions; + public static synthetic fun copy-9VgGkz4$default (Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;JILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions; public fun equals (Ljava/lang/Object;)Z - public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params; - public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; + public final fun getOnProgress ()Lkotlin/jvm/functions/Function1; + public final fun getTimeout-UwyO8pc ()J public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$$serializer; +public final class io/modelcontextprotocol/kotlin/sdk/shared/TooLongFrameException : java/io/IOException { + public fun (JI)V + public final fun getFrameSize ()J + public final fun getMaxFrameSize ()I +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/shared/Transport { + public abstract fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun onClose (Lkotlin/jvm/functions/Function0;)V + public abstract fun onError (Lkotlin/jvm/functions/Function1;)V + public abstract fun onMessage (Lkotlin/jvm/functions/Function2;)V + public abstract fun send (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Lio/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun send$default (Lio/modelcontextprotocol/kotlin/sdk/shared/Transport;Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Lio/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public abstract fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class io/modelcontextprotocol/kotlin/sdk/shared/Transport$DefaultImpls { + public static synthetic fun send$default (Lio/modelcontextprotocol/kotlin/sdk/shared/Transport;Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Lio/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; +} + +public class io/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions { + public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestId; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Lkotlin/jvm/functions/Function1; + public fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions;Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions; + public fun equals (Ljava/lang/Object;)Z + public final fun getOnResumptionToken ()Lkotlin/jvm/functions/Function1; + public final fun getRelatedRequestId ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestId; + public final fun getResumptionToken ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public abstract class io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { + public fun ()V + public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + protected abstract fun getSession ()Lio/ktor/websocket/WebSocketSession; + protected abstract fun initializeSession (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun send (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Lio/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransportKt { + public static final field MCP_SUBPROTOCOL Ljava/lang/String; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Annotations { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/Annotations$Companion; + public fun ()V + public fun (Ljava/util/List;Ljava/lang/Double;Ljava/lang/String;)V + public synthetic fun (Ljava/util/List;Ljava/lang/Double;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/List; + public final fun component2 ()Ljava/lang/Double; + public final fun component3 ()Ljava/lang/String; + public final fun copy (Ljava/util/List;Ljava/lang/Double;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/Annotations; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Ljava/util/List;Ljava/lang/Double;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/Annotations; + public fun equals (Ljava/lang/Object;)Z + public final fun getAudience ()Ljava/util/List; + public final fun getLastModified ()Ljava/lang/String; + public final fun getPriority ()Ljava/lang/Double; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/Annotations$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/Annotations$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/Annotations; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/Annotations$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params : io/modelcontextprotocol/kotlin/sdk/NotificationParams { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params$Companion; - public fun (Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel; +public final class io/modelcontextprotocol/kotlin/sdk/types/AudioContent : io/modelcontextprotocol/kotlin/sdk/types/MediaContent { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/AudioContent$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Lkotlinx/serialization/json/JsonElement; + public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/types/Annotations; public final fun component4 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params;Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/AudioContent; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/AudioContent;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/AudioContent; public fun equals (Ljava/lang/Object;)Z - public final fun getData ()Lkotlinx/serialization/json/JsonElement; - public final fun getLevel ()Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel; - public final fun getLogger ()Ljava/lang/String; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; + public final fun getAnnotations ()Lio/modelcontextprotocol/kotlin/sdk/types/Annotations; + public final fun getData ()Ljava/lang/String; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public final fun getMimeType ()Ljava/lang/String; + public fun getType ()Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/AudioContent$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/AudioContent$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/AudioContent; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/AudioContent;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/AudioContent$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$SetLevelRequest : io/modelcontextprotocol/kotlin/sdk/ClientRequest, io/modelcontextprotocol/kotlin/sdk/WithMeta { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$SetLevelRequest$Companion; - public fun (Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel; - public final fun component2 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$SetLevelRequest; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$SetLevelRequest;Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$SetLevelRequest; - public fun equals (Ljava/lang/Object;)Z - public final fun getLevel ()Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel; - public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$SetLevelRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$SetLevelRequest$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$SetLevelRequest; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$SetLevelRequest;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$SetLevelRequest$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/McpError : java/lang/Exception { - public fun (ILjava/lang/String;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (ILjava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getCode ()I - public final fun getData ()Lkotlinx/serialization/json/JsonObject; - public fun getMessage ()Ljava/lang/String; -} - -public abstract interface class io/modelcontextprotocol/kotlin/sdk/Method { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Method$Companion; - public abstract fun getValue ()Ljava/lang/String; -} - -public final class io/modelcontextprotocol/kotlin/sdk/Method$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/Method$Custom : io/modelcontextprotocol/kotlin/sdk/Method { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Method$Custom$Companion; - public fun (Ljava/lang/String;)V - public final fun component1 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/Method$Custom; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/Method$Custom;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/Method$Custom; - public fun equals (Ljava/lang/Object;)Z - public fun getValue ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/Method$Custom$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/Method$Custom$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/Method$Custom; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/Method$Custom;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/Method$Custom$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/Method$Defined : java/lang/Enum, io/modelcontextprotocol/kotlin/sdk/Method { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Method$Defined$Companion; - public static final field CompletionComplete Lio/modelcontextprotocol/kotlin/sdk/Method$Defined; - public static final field ElicitationCreate Lio/modelcontextprotocol/kotlin/sdk/Method$Defined; - public static final field Initialize Lio/modelcontextprotocol/kotlin/sdk/Method$Defined; - public static final field LoggingSetLevel Lio/modelcontextprotocol/kotlin/sdk/Method$Defined; - public static final field NotificationsCancelled Lio/modelcontextprotocol/kotlin/sdk/Method$Defined; - public static final field NotificationsInitialized Lio/modelcontextprotocol/kotlin/sdk/Method$Defined; - public static final field NotificationsMessage Lio/modelcontextprotocol/kotlin/sdk/Method$Defined; - public static final field NotificationsProgress Lio/modelcontextprotocol/kotlin/sdk/Method$Defined; - public static final field NotificationsPromptsListChanged Lio/modelcontextprotocol/kotlin/sdk/Method$Defined; - public static final field NotificationsResourcesListChanged Lio/modelcontextprotocol/kotlin/sdk/Method$Defined; - public static final field NotificationsResourcesUpdated Lio/modelcontextprotocol/kotlin/sdk/Method$Defined; - public static final field NotificationsRootsListChanged Lio/modelcontextprotocol/kotlin/sdk/Method$Defined; - public static final field NotificationsToolsListChanged Lio/modelcontextprotocol/kotlin/sdk/Method$Defined; - public static final field Ping Lio/modelcontextprotocol/kotlin/sdk/Method$Defined; - public static final field PromptsGet Lio/modelcontextprotocol/kotlin/sdk/Method$Defined; - public static final field PromptsList Lio/modelcontextprotocol/kotlin/sdk/Method$Defined; - public static final field ResourcesList Lio/modelcontextprotocol/kotlin/sdk/Method$Defined; - public static final field ResourcesRead Lio/modelcontextprotocol/kotlin/sdk/Method$Defined; - public static final field ResourcesSubscribe Lio/modelcontextprotocol/kotlin/sdk/Method$Defined; - public static final field ResourcesTemplatesList Lio/modelcontextprotocol/kotlin/sdk/Method$Defined; - public static final field ResourcesUnsubscribe Lio/modelcontextprotocol/kotlin/sdk/Method$Defined; - public static final field RootsList Lio/modelcontextprotocol/kotlin/sdk/Method$Defined; - public static final field SamplingCreateMessage Lio/modelcontextprotocol/kotlin/sdk/Method$Defined; - public static final field ToolsCall Lio/modelcontextprotocol/kotlin/sdk/Method$Defined; - public static final field ToolsList Lio/modelcontextprotocol/kotlin/sdk/Method$Defined; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public fun getValue ()Ljava/lang/String; - public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/Method$Defined; - public static fun values ()[Lio/modelcontextprotocol/kotlin/sdk/Method$Defined; -} - -public final class io/modelcontextprotocol/kotlin/sdk/Method$Defined$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ModelHint { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ModelHint$Companion; - public fun (Ljava/lang/String;)V - public final fun component1 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/ModelHint; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ModelHint;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ModelHint; - public fun equals (Ljava/lang/Object;)Z - public final fun getName ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ModelHint$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ModelHint$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ModelHint; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ModelHint;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ModelHint$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ModelPreferences { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ModelPreferences$Companion; - public fun (Ljava/util/List;Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Double;)V - public final fun getCostPriority ()Ljava/lang/Double; - public final fun getHints ()Ljava/util/List; - public final fun getIntelligencePriority ()Ljava/lang/Double; - public final fun getSpeedPriority ()Ljava/lang/Double; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ModelPreferences$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ModelPreferences$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ModelPreferences; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ModelPreferences;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ModelPreferences$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public abstract interface class io/modelcontextprotocol/kotlin/sdk/Notification { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Notification$Companion; - public abstract fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public abstract fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; -} - -public final class io/modelcontextprotocol/kotlin/sdk/Notification$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public abstract interface class io/modelcontextprotocol/kotlin/sdk/NotificationParams : io/modelcontextprotocol/kotlin/sdk/WithMeta { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/NotificationParams$Companion; -} - -public final class io/modelcontextprotocol/kotlin/sdk/NotificationParams$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public abstract interface class io/modelcontextprotocol/kotlin/sdk/PaginatedRequest : io/modelcontextprotocol/kotlin/sdk/Request, io/modelcontextprotocol/kotlin/sdk/WithMeta { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/PaginatedRequest$Companion; - public abstract fun getCursor ()Ljava/lang/String; - public abstract fun get_meta ()Lkotlinx/serialization/json/JsonObject; -} - -public final class io/modelcontextprotocol/kotlin/sdk/PaginatedRequest$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public abstract interface class io/modelcontextprotocol/kotlin/sdk/PaginatedResult : io/modelcontextprotocol/kotlin/sdk/RequestResult { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/PaginatedResult$Companion; - public abstract fun getNextCursor ()Ljava/lang/String; -} - -public final class io/modelcontextprotocol/kotlin/sdk/PaginatedResult$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/PingRequest : io/modelcontextprotocol/kotlin/sdk/ClientRequest, io/modelcontextprotocol/kotlin/sdk/ServerRequest { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/PingRequest$Companion; +public final class io/modelcontextprotocol/kotlin/sdk/types/AudioContentBuilder : io/modelcontextprotocol/kotlin/sdk/types/MediaContentBuilder { public fun ()V - public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; + public synthetic fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent; + public final fun getData ()Ljava/lang/String; + public final fun getMimeType ()Ljava/lang/String; + public final fun setData (Ljava/lang/String;)V + public final fun setMimeType (Ljava/lang/String;)V } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/PingRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/PingRequest$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/PingRequest; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/PingRequest;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/PingRequest$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public class io/modelcontextprotocol/kotlin/sdk/Progress : io/modelcontextprotocol/kotlin/sdk/ProgressBase { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Progress$Companion; - public fun (DLjava/lang/Double;Ljava/lang/String;)V - public synthetic fun (IDLjava/lang/Double;Ljava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V - public fun getMessage ()Ljava/lang/String; - public fun getProgress ()D - public fun getTotal ()Ljava/lang/Double; - public static final synthetic fun write$Self (Lio/modelcontextprotocol/kotlin/sdk/Progress;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/Progress$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/Progress$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/Progress; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/Progress;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/Progress$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public abstract interface class io/modelcontextprotocol/kotlin/sdk/ProgressBase { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ProgressBase$Companion; - public abstract fun getMessage ()Ljava/lang/String; - public abstract fun getProgress ()D - public abstract fun getTotal ()Ljava/lang/Double; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ProgressBase$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ProgressNotification : io/modelcontextprotocol/kotlin/sdk/ClientNotification, io/modelcontextprotocol/kotlin/sdk/ServerNotification { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Companion; - public fun (Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params;)Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification;Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification; - public fun equals (Ljava/lang/Object;)Z - public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; - public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ProgressNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ProgressNotification$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params : io/modelcontextprotocol/kotlin/sdk/NotificationParams, io/modelcontextprotocol/kotlin/sdk/ProgressBase { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params$Companion; - public fun (DLio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/Double;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (DLio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/Double;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()D - public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/RequestId; - public final fun component3 ()Ljava/lang/Double; - public final fun component4 ()Ljava/lang/String; - public final fun component5 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (DLio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/Double;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params;DLio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/Double;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params; - public fun equals (Ljava/lang/Object;)Z - public fun getMessage ()Ljava/lang/String; - public fun getProgress ()D - public final fun getProgressToken ()Lio/modelcontextprotocol/kotlin/sdk/RequestId; - public fun getTotal ()Ljava/lang/Double; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/Prompt { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Prompt$Companion; - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V - public final fun getArguments ()Ljava/util/List; - public final fun getDescription ()Ljava/lang/String; - public final fun getName ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/Prompt$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/Prompt$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/Prompt; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/Prompt;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/Prompt$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/PromptArgument { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/PromptArgument$Companion; - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Ljava/lang/Boolean; - public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;)Lio/modelcontextprotocol/kotlin/sdk/PromptArgument; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/PromptArgument;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/PromptArgument; - public fun equals (Ljava/lang/Object;)Z - public final fun getDescription ()Ljava/lang/String; - public final fun getName ()Ljava/lang/String; - public final fun getRequired ()Ljava/lang/Boolean; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/PromptArgument$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/PromptArgument$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/PromptArgument; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/PromptArgument;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/PromptArgument$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification : io/modelcontextprotocol/kotlin/sdk/ServerNotification { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Companion; - public fun ()V - public fun (Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params;)Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification;Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification; - public fun equals (Ljava/lang/Object;)Z - public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; - public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params : io/modelcontextprotocol/kotlin/sdk/NotificationParams { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params$Companion; +public final class io/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams : io/modelcontextprotocol/kotlin/sdk/types/NotificationParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams$Companion; public fun ()V public fun (Lkotlinx/serialization/json/JsonObject;)V public synthetic fun (Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params; + public final fun copy (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; public fun equals (Ljava/lang/Object;)Z - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/PromptMessage { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/PromptMessage$Companion; - public fun (Lio/modelcontextprotocol/kotlin/sdk/Role;Lio/modelcontextprotocol/kotlin/sdk/PromptMessageContent;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/Role; - public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/PromptMessageContent; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/Role;Lio/modelcontextprotocol/kotlin/sdk/PromptMessageContent;)Lio/modelcontextprotocol/kotlin/sdk/PromptMessage; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/PromptMessage;Lio/modelcontextprotocol/kotlin/sdk/Role;Lio/modelcontextprotocol/kotlin/sdk/PromptMessageContent;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/PromptMessage; +public final class io/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams : io/modelcontextprotocol/kotlin/sdk/types/RequestParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams$Companion; + public synthetic fun (Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun copy-mJxasco (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams; + public static synthetic fun copy-mJxasco$default (Lio/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams; public fun equals (Ljava/lang/Object;)Z - public final fun getContent ()Lio/modelcontextprotocol/kotlin/sdk/PromptMessageContent; - public final fun getRole ()Lio/modelcontextprotocol/kotlin/sdk/Role; + public fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/PromptMessage$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/PromptMessage$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/PromptMessage; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/PromptMessage;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/PromptMessage$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public abstract interface class io/modelcontextprotocol/kotlin/sdk/PromptMessageContent { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/PromptMessageContent$Companion; - public abstract fun getType ()Ljava/lang/String; -} - -public final class io/modelcontextprotocol/kotlin/sdk/PromptMessageContent$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public abstract interface class io/modelcontextprotocol/kotlin/sdk/PromptMessageContentMultimodal : io/modelcontextprotocol/kotlin/sdk/PromptMessageContent { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/PromptMessageContentMultimodal$Companion; -} - -public final class io/modelcontextprotocol/kotlin/sdk/PromptMessageContentMultimodal$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/PromptReference : io/modelcontextprotocol/kotlin/sdk/Reference { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/PromptReference$Companion; - public static final field TYPE Ljava/lang/String; - public fun (Ljava/lang/String;)V +public final class io/modelcontextprotocol/kotlin/sdk/types/BlobResourceContents : io/modelcontextprotocol/kotlin/sdk/types/ResourceContents { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/BlobResourceContents$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/PromptReference; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/PromptReference;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/PromptReference; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/BlobResourceContents; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/BlobResourceContents;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/BlobResourceContents; public fun equals (Ljava/lang/Object;)Z - public final fun getName ()Ljava/lang/String; - public fun getType ()Ljava/lang/String; + public final fun getBlob ()Ljava/lang/String; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public fun getMimeType ()Ljava/lang/String; + public fun getUri ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/PromptReference$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/PromptReference$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/BlobResourceContents$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/BlobResourceContents$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/PromptReference; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/BlobResourceContents; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/PromptReference;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/BlobResourceContents;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/PromptReference$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/BlobResourceContents$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ReadResourceRequest : io/modelcontextprotocol/kotlin/sdk/ClientRequest, io/modelcontextprotocol/kotlin/sdk/WithMeta { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ReadResourceRequest$Companion; - public fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +public final class io/modelcontextprotocol/kotlin/sdk/types/BooleanSchema : io/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaDefinition { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/BooleanSchema$Companion; + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/Boolean; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;)Lio/modelcontextprotocol/kotlin/sdk/types/BooleanSchema; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/BooleanSchema;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/BooleanSchema; + public fun equals (Ljava/lang/Object;)Z + public final fun getDefault ()Ljava/lang/Boolean; + public final fun getDescription ()Ljava/lang/String; + public final fun getTitle ()Ljava/lang/String; + public final fun getType ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/BooleanSchema$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/BooleanSchema$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/BooleanSchema; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/BooleanSchema;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/BooleanSchema$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CacheScope : java/lang/Enum { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CacheScope$Companion; + public static final field Private Lio/modelcontextprotocol/kotlin/sdk/types/CacheScope; + public static final field Public Lio/modelcontextprotocol/kotlin/sdk/types/CacheScope; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/CacheScope; + public static fun values ()[Lio/modelcontextprotocol/kotlin/sdk/types/CacheScope; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CacheScope$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CallToolRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequest$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequest; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequest;Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequest; + public fun equals (Ljava/lang/Object;)Z + public final fun getArguments ()Lkotlinx/serialization/json/JsonObject; + public final fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public final fun getName ()Ljava/lang/String; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams; + public final fun getTask ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/CallToolRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequest$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequest; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequest;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CallToolRequest$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CallToolRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/RequestBuilder { + public fun ()V + public final fun arguments (Lkotlin/jvm/functions/Function1;)V + public final fun arguments (Lkotlinx/serialization/json/JsonObject;)V + public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequest; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; + public final fun getName ()Ljava/lang/String; + public final fun setName (Ljava/lang/String;)V +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams : io/modelcontextprotocol/kotlin/sdk/types/RequestParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams$Companion; + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/ReadResourceRequest; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ReadResourceRequest;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ReadResourceRequest; + public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata; + public final fun component4-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun copy-6R815Ik (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams; + public static synthetic fun copy-6R815Ik$default (Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams; public fun equals (Ljava/lang/Object;)Z - public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public final fun getUri ()Ljava/lang/String; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; + public final fun getArguments ()Lkotlinx/serialization/json/JsonObject; + public fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun getName ()Ljava/lang/String; + public final fun getTask ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata; public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ReadResourceRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ReadResourceRequest$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ReadResourceRequest; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ReadResourceRequest;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ReadResourceRequest$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ReadResourceResult : io/modelcontextprotocol/kotlin/sdk/ServerResult { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ReadResourceResult$Companion; - public fun (Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getContents ()Ljava/util/List; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ReadResourceResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ReadResourceResult$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ReadResourceResult; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ReadResourceResult;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ReadResourceResult$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public abstract interface class io/modelcontextprotocol/kotlin/sdk/Reference { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Reference$Companion; - public abstract fun getType ()Ljava/lang/String; -} - -public final class io/modelcontextprotocol/kotlin/sdk/Reference$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public abstract interface class io/modelcontextprotocol/kotlin/sdk/Request { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Request$Companion; - public abstract fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; -} - -public final class io/modelcontextprotocol/kotlin/sdk/Request$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public abstract interface class io/modelcontextprotocol/kotlin/sdk/RequestId { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/RequestId$Companion; -} - -public final class io/modelcontextprotocol/kotlin/sdk/RequestId$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/RequestId$NumberId : io/modelcontextprotocol/kotlin/sdk/RequestId { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/RequestId$NumberId$Companion; - public fun (J)V - public final fun component1 ()J - public final fun copy (J)Lio/modelcontextprotocol/kotlin/sdk/RequestId$NumberId; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/RequestId$NumberId;JILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/RequestId$NumberId; +public final class io/modelcontextprotocol/kotlin/sdk/types/CallToolResult : io/modelcontextprotocol/kotlin/sdk/types/ServerResult { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult$Companion; + public fun (Ljava/util/List;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/util/List;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/List; + public final fun component2 ()Ljava/lang/Boolean; + public final fun component3 ()Lkotlinx/serialization/json/JsonObject; + public final fun component4 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/util/List;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult;Ljava/util/List;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult; public fun equals (Ljava/lang/Object;)Z - public final fun getValue ()J + public final fun getContent ()Ljava/util/List; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public final fun getStructuredContent ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public final fun isError ()Ljava/lang/Boolean; + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/CallToolResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CallToolResult$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest, io/modelcontextprotocol/kotlin/sdk/types/ServerRequest { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequest$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequestParams;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequestParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequest; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequest;Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequest; + public fun equals (Ljava/lang/Object;)Z + public final fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequestParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams; + public final fun getTaskId ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/RequestId$NumberId$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/RequestId$NumberId$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequest$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/RequestId$NumberId; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequest; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/RequestId$NumberId;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequest;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/RequestId$NumberId$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequest$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/RequestId$StringId : io/modelcontextprotocol/kotlin/sdk/RequestId { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/RequestId$StringId$Companion; - public fun (Ljava/lang/String;)V +public final class io/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequestParams : io/modelcontextprotocol/kotlin/sdk/types/RequestParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequestParams$Companion; + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/RequestId$StringId; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/RequestId$StringId;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/RequestId$StringId; + public final fun component2-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun copy-D-90kyg (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequestParams; + public static synthetic fun copy-D-90kyg$default (Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequestParams;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequestParams; public fun equals (Ljava/lang/Object;)Z + public fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun getTaskId ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequestParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequestParams$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequestParams; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequestParams;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequestParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CancelledNotification : io/modelcontextprotocol/kotlin/sdk/types/ClientNotification, io/modelcontextprotocol/kotlin/sdk/types/ServerNotification { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotification$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotificationParams;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotificationParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotification;Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotificationParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotification; + public fun equals (Ljava/lang/Object;)Z + public final fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotificationParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/NotificationParams; + public final fun getReason ()Ljava/lang/String; + public final fun getRequestId ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestId; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/CancelledNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotification$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotification; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotification;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CancelledNotification$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CancelledNotificationParams : io/modelcontextprotocol/kotlin/sdk/types/NotificationParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotificationParams$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestId; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotificationParams; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotificationParams;Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotificationParams; + public fun equals (Ljava/lang/Object;)Z + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public final fun getReason ()Ljava/lang/String; + public final fun getRequestId ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestId; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/CancelledNotificationParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotificationParams$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotificationParams; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotificationParams;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CancelledNotificationParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CapabilitiesKt { + public static final fun getSupportsUrl (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation;)Z +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Companion; + public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;)V + public synthetic fun (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling; + public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots; + public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation; + public final fun component4 ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks; + public final fun component5 ()Lkotlinx/serialization/json/JsonObject; + public final fun component6 ()Ljava/util/Map; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; + public fun equals (Ljava/lang/Object;)Z + public final fun getElicitation ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation; + public final fun getExperimental ()Lkotlinx/serialization/json/JsonObject; + public final fun getExtensions ()Ljava/util/Map; + public final fun getRoots ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots; + public final fun getSampling ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling; + public final fun getTasks ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Companion { + public final fun getElicitation ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation; + public final fun getSampling ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling; + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation$Companion; + public fun ()V + public fun (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lkotlinx/serialization/json/JsonObject; + public final fun component2 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation; + public fun equals (Ljava/lang/Object;)Z + public final fun getForm ()Lkotlinx/serialization/json/JsonObject; + public final fun getUrl ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots$Companion; + public fun ()V + public fun (Ljava/lang/Boolean;)V + public synthetic fun (Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/Boolean; + public final fun copy (Ljava/lang/Boolean;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Ljava/lang/Boolean;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots; + public fun equals (Ljava/lang/Object;)Z + public final fun getListChanged ()Ljava/lang/Boolean; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling$Companion; + public fun ()V + public fun (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lkotlinx/serialization/json/JsonObject; + public final fun component2 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling; + public fun equals (Ljava/lang/Object;)Z + public final fun getContext ()Lkotlinx/serialization/json/JsonObject; + public final fun getTools ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Companion; + public fun ()V + public fun (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests;)V + public synthetic fun (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lkotlinx/serialization/json/JsonObject; + public final fun component2 ()Lkotlinx/serialization/json/JsonObject; + public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests; + public final fun copy (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks; + public fun equals (Ljava/lang/Object;)Z + public final fun getCancel ()Lkotlinx/serialization/json/JsonObject; + public final fun getList ()Lkotlinx/serialization/json/JsonObject; + public final fun getRequests ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Companion; + public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling; + public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests; + public fun equals (Ljava/lang/Object;)Z + public final fun getElicitation ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation; + public final fun getSampling ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation$Companion; + public fun ()V + public fun (Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation; + public fun equals (Ljava/lang/Object;)Z + public final fun getCreate ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling$Companion; + public fun ()V + public fun (Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling; + public fun equals (Ljava/lang/Object;)Z + public final fun getCreateMessage ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilitiesBuilder { + public fun ()V + public final fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; + public final fun elicitation (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation;)V + public final fun experimental (Lkotlin/jvm/functions/Function1;)V + public final fun experimental (Lkotlinx/serialization/json/JsonObject;)V + public final fun extensions (Ljava/util/Map;)V + public final fun roots (Ljava/lang/Boolean;)V + public static synthetic fun roots$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilitiesBuilder;Ljava/lang/Boolean;ILjava/lang/Object;)V + public final fun sampling (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;)V +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/ClientNotification : io/modelcontextprotocol/kotlin/sdk/types/Notification { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ClientNotification$Companion; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ClientNotification$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/ClientRequest : io/modelcontextprotocol/kotlin/sdk/types/Request { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ClientRequest$Companion; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ClientRequest$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/ClientResult : io/modelcontextprotocol/kotlin/sdk/types/RequestResult { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ClientResult$Companion; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ClientResult$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CommonKt { + public static final field DEFAULT_NEGOTIATED_PROTOCOL_VERSION Ljava/lang/String; + public static final field LATEST_PROTOCOL_VERSION Ljava/lang/String; + public static final fun ProgressToken (J)Lio/modelcontextprotocol/kotlin/sdk/types/RequestId; + public static final fun ProgressToken (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/RequestId; + public static final fun getSUPPORTED_PROTOCOL_VERSIONS ()Ljava/util/List; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CompleteRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequest$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequest; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequest;Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequest; + public fun equals (Ljava/lang/Object;)Z + public final fun getArgument ()Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Argument; + public final fun getContext ()Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Context; + public final fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams; + public final fun getRef ()Lio/modelcontextprotocol/kotlin/sdk/types/Reference; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/CompleteRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequest$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequest; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequest;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CompleteRequest$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CompleteRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/RequestBuilder { + public fun ()V + public final fun argument (Ljava/lang/String;Ljava/lang/String;)V + public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequest; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; + public final fun context (Ljava/util/Map;)V + public final fun context (Lkotlin/jvm/functions/Function1;)V + public final fun ref (Lio/modelcontextprotocol/kotlin/sdk/types/Reference;)V +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams : io/modelcontextprotocol/kotlin/sdk/types/RequestParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Companion; + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Argument;Lio/modelcontextprotocol/kotlin/sdk/types/Reference;Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Context;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Argument;Lio/modelcontextprotocol/kotlin/sdk/types/Reference;Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Context;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Argument; + public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/Reference; + public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Context; + public final fun component4-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun copy-6R815Ik (Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Argument;Lio/modelcontextprotocol/kotlin/sdk/types/Reference;Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Context;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams; + public static synthetic fun copy-6R815Ik$default (Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams;Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Argument;Lio/modelcontextprotocol/kotlin/sdk/types/Reference;Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Context;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams; + public fun equals (Ljava/lang/Object;)Z + public final fun getArgument ()Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Argument; + public final fun getContext ()Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Context; + public fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun getRef ()Lio/modelcontextprotocol/kotlin/sdk/types/Reference; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Argument { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Argument$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Argument; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Argument;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Argument; + public fun equals (Ljava/lang/Object;)Z + public final fun getName ()Ljava/lang/String; public final fun getValue ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/RequestId$StringId$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/RequestId$StringId$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Argument$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Argument$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/RequestId$StringId; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Argument; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/RequestId$StringId;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Argument;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/RequestId$StringId$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Argument$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/RequestIdSerializer : kotlinx/serialization/KSerializer { +public final class io/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Context { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Context$Companion; public fun ()V - public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/RequestId; + public fun (Ljava/util/Map;)V + public synthetic fun (Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/Map; + public final fun copy (Ljava/util/Map;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Context; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Context;Ljava/util/Map;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Context; + public fun equals (Ljava/lang/Object;)Z + public final fun getArguments ()Ljava/util/Map; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Context$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Context$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Context; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/RequestId;)V + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Context;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public abstract interface class io/modelcontextprotocol/kotlin/sdk/RequestResult : io/modelcontextprotocol/kotlin/sdk/WithMeta { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/RequestResult$Companion; -} - -public final class io/modelcontextprotocol/kotlin/sdk/RequestResult$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Context$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/Resource { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Resource$Companion; - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Lio/modelcontextprotocol/kotlin/sdk/Annotations;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Lio/modelcontextprotocol/kotlin/sdk/Annotations;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +public final class io/modelcontextprotocol/kotlin/sdk/types/CompleteResult : io/modelcontextprotocol/kotlin/sdk/types/ServerResult { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult$Completion;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult$Completion;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult$Completion; + public final fun component2 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult$Completion;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult;Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult$Completion;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult; + public fun equals (Ljava/lang/Object;)Z + public final fun getCompletion ()Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult$Completion; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/CompleteResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CompleteResult$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CompleteResult$Completion { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult$Completion$Companion; + public fun (Ljava/util/List;Ljava/lang/Integer;Ljava/lang/Boolean;)V + public synthetic fun (Ljava/util/List;Ljava/lang/Integer;Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/List; + public final fun component2 ()Ljava/lang/Integer; + public final fun component3 ()Ljava/lang/Boolean; + public final fun copy (Ljava/util/List;Ljava/lang/Integer;Ljava/lang/Boolean;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult$Completion; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult$Completion;Ljava/util/List;Ljava/lang/Integer;Ljava/lang/Boolean;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult$Completion; + public fun equals (Ljava/lang/Object;)Z + public final fun getHasMore ()Ljava/lang/Boolean; + public final fun getTotal ()Ljava/lang/Integer; + public final fun getValues ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/CompleteResult$Completion$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult$Completion$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult$Completion; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult$Completion;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CompleteResult$Completion$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Completion_dslKt { + public static final fun buildCompleteRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequest; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/ContentBlock : io/modelcontextprotocol/kotlin/sdk/types/WithMeta { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ContentBlock$Companion; + public abstract fun getType ()Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ContentBlock$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ContentTypes : java/lang/Enum { + public static final field AUDIO Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes$Companion; + public static final field EMBEDDED_RESOURCE Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; + public static final field IMAGE Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; + public static final field RESOURCE_LINK Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; + public static final field TEXT Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; + public static final field TOOL_RESULT Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; + public static final field TOOL_USE Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public final fun getValue ()Ljava/lang/String; + public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; + public static fun values ()[Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ContentTypes$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest : io/modelcontextprotocol/kotlin/sdk/types/ServerRequest { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest; + public fun equals (Ljava/lang/Object;)Z + public final fun getIncludeContext ()Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext; + public final fun getMaxTokens ()I + public final fun getMessages ()Ljava/util/List; + public final fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun getMetadata ()Lkotlinx/serialization/json/JsonObject; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public final fun getModelPreferences ()Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams; + public final fun getStopSequences ()Ljava/util/List; + public final fun getSystemPrompt ()Ljava/lang/String; + public final fun getTask ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata; + public final fun getTemperature ()Ljava/lang/Double; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/RequestBuilder { + public fun ()V + public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; + public final fun getContext ()Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext; + public final fun getMaxTokens ()Ljava/lang/Integer; + public final fun getStopSequences ()Ljava/util/List; + public final fun getSystemPrompt ()Ljava/lang/String; + public final fun getTemperature ()Ljava/lang/Double; + public final fun messages (Ljava/util/List;)V + public final fun messages (Lkotlin/jvm/functions/Function1;)V + public final fun metadata (Lkotlin/jvm/functions/Function1;)V + public final fun preferences (Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences;)V + public final fun preferences (Ljava/util/List;Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Double;)V + public static synthetic fun preferences$default (Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestBuilder;Ljava/util/List;Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Double;ILjava/lang/Object;)V + public final fun setContext (Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext;)V + public final fun setMaxTokens (Ljava/lang/Integer;)V + public final fun setStopSequences (Ljava/util/List;)V + public final fun setSystemPrompt (Ljava/lang/String;)V + public final fun setTemperature (Ljava/lang/Double;)V +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams : io/modelcontextprotocol/kotlin/sdk/types/RequestParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams$Companion; + public synthetic fun (ILjava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext;Ljava/lang/Double;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (ILjava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext;Ljava/lang/Double;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()I + public final fun component10 ()Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice; + public final fun component11 ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata; + public final fun component12-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun component2 ()Ljava/util/List; + public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext; + public final fun component6 ()Ljava/lang/Double; + public final fun component7 ()Ljava/util/List; + public final fun component8 ()Lkotlinx/serialization/json/JsonObject; + public final fun component9 ()Ljava/util/List; + public final fun copy-7OuPiQU (ILjava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext;Ljava/lang/Double;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams; + public static synthetic fun copy-7OuPiQU$default (Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams;ILjava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext;Ljava/lang/Double;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams; + public fun equals (Ljava/lang/Object;)Z + public final fun getIncludeContext ()Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext; + public final fun getMaxTokens ()I + public final fun getMessages ()Ljava/util/List; + public fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun getMetadata ()Lkotlinx/serialization/json/JsonObject; + public final fun getModelPreferences ()Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences; + public final fun getStopSequences ()Ljava/util/List; + public final fun getSystemPrompt ()Ljava/lang/String; + public final fun getTask ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata; + public final fun getTemperature ()Ljava/lang/Double; + public final fun getToolChoice ()Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice; + public final fun getTools ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageResult : io/modelcontextprotocol/kotlin/sdk/types/ClientResult { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageResult$Companion; + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/Role; + public final fun component2 ()Ljava/util/List; + public final fun component3 ()Ljava/lang/String; + public final fun component4-6olV-UY ()Ljava/lang/String; + public final fun component5 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy-ctVX1tw (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageResult; + public static synthetic fun copy-ctVX1tw$default (Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageResult;Lio/modelcontextprotocol/kotlin/sdk/types/Role;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageResult; + public fun equals (Ljava/lang/Object;)Z + public final fun getContent ()Ljava/util/List; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public final fun getModel ()Ljava/lang/String; + public final fun getRole ()Lio/modelcontextprotocol/kotlin/sdk/types/Role; + public final fun getStopReason-6olV-UY ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageResult$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageResult; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageResult;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageResult$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CreateTaskResult : io/modelcontextprotocol/kotlin/sdk/types/ClientResult, io/modelcontextprotocol/kotlin/sdk/types/ServerResult { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CreateTaskResult$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/Task;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Task;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/Task; + public final fun component2 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/Task;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateTaskResult; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/CreateTaskResult;Lio/modelcontextprotocol/kotlin/sdk/types/Task;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateTaskResult; + public fun equals (Ljava/lang/Object;)Z + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public final fun getTask ()Lio/modelcontextprotocol/kotlin/sdk/types/Task; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/CreateTaskResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/CreateTaskResult$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateTaskResult; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/CreateTaskResult;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CreateTaskResult$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CustomNotification : io/modelcontextprotocol/kotlin/sdk/types/ClientNotification, io/modelcontextprotocol/kotlin/sdk/types/ServerNotification { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CustomNotification$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/Method;Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Method;Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/Method;Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/CustomNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/CustomNotification;Lio/modelcontextprotocol/kotlin/sdk/types/Method;Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CustomNotification; + public fun equals (Ljava/lang/Object;)Z + public final fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/NotificationParams; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/CustomNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/CustomNotification$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/CustomNotification; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/CustomNotification;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CustomNotification$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public class io/modelcontextprotocol/kotlin/sdk/types/CustomRequest : io/modelcontextprotocol/kotlin/sdk/types/Request { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CustomRequest$Companion; + public synthetic fun (ILio/modelcontextprotocol/kotlin/sdk/types/Method;Lio/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/Method;Lio/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams;)V + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams; + public static final synthetic fun write$Self (Lio/modelcontextprotocol/kotlin/sdk/types/CustomRequest;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/CustomRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/CustomRequest$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/CustomRequest; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/CustomRequest;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CustomRequest$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest;Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest; + public fun equals (Ljava/lang/Object;)Z + public final fun getMeta--Khlwt8 ()Lkotlinx/serialization/json/JsonObject; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams : io/modelcontextprotocol/kotlin/sdk/types/RequestParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams$Companion; + public synthetic fun (Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1--Khlwt8 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy-hVVZdjo (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams; + public static synthetic fun copy-hVVZdjo$default (Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams; + public fun equals (Ljava/lang/Object;)Z + public fun getMeta--Khlwt8 ()Lkotlinx/serialization/json/JsonObject; + public synthetic fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/DiscoverResult : io/modelcontextprotocol/kotlin/sdk/types/ServerResult { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverResult$Companion; + public fun (Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;Ljava/lang/String;Ljava/lang/String;JLio/modelcontextprotocol/kotlin/sdk/types/CacheScope;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;Ljava/lang/String;Ljava/lang/String;JLio/modelcontextprotocol/kotlin/sdk/types/CacheScope;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/List; + public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()J + public final fun component6 ()Lio/modelcontextprotocol/kotlin/sdk/types/CacheScope; + public final fun component7 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;Ljava/lang/String;Ljava/lang/String;JLio/modelcontextprotocol/kotlin/sdk/types/CacheScope;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverResult; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverResult;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;Ljava/lang/String;Ljava/lang/String;JLio/modelcontextprotocol/kotlin/sdk/types/CacheScope;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverResult; + public fun equals (Ljava/lang/Object;)Z + public final fun getCacheScope ()Lio/modelcontextprotocol/kotlin/sdk/types/CacheScope; + public final fun getCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities; + public final fun getInstructions ()Ljava/lang/String; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public final fun getResultType ()Ljava/lang/String; + public final fun getSupportedVersions ()Ljava/util/List; + public final fun getTtlMs ()J + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/DiscoverResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverResult$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverResult; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverResult;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/DiscoverResult$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/DoubleSchema : io/modelcontextprotocol/kotlin/sdk/types/NumberSchemaDefinition { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/DoubleSchema$Companion; + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Double;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Double;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/Double; + public final fun component4 ()Ljava/lang/Double; + public final fun component5 ()Ljava/lang/Double; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Double;)Lio/modelcontextprotocol/kotlin/sdk/types/DoubleSchema; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/DoubleSchema;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Double;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/DoubleSchema; + public fun equals (Ljava/lang/Object;)Z + public final fun getDefault ()Ljava/lang/Double; + public final fun getDescription ()Ljava/lang/String; + public final fun getMaximum ()Ljava/lang/Double; + public final fun getMinimum ()Ljava/lang/Double; + public final fun getTitle ()Ljava/lang/String; + public final fun getType ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/DoubleSchema$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/DoubleSchema$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/DoubleSchema; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/DoubleSchema;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/DoubleSchema$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequest : io/modelcontextprotocol/kotlin/sdk/types/ServerRequest { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequest$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequest; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequest;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequest; + public fun equals (Ljava/lang/Object;)Z + public final fun getMessage ()Ljava/lang/String; + public final fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams; + public final fun getRequestedSchema ()Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequest$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequest; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequest;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequest$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/RequestBuilder { + public fun ()V + public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequest; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; + public final fun getMessage ()Ljava/lang/String; + public final fun requestedSchema (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;)V + public final fun requestedSchema (Lkotlin/jvm/functions/Function1;)V + public final fun setMessage (Ljava/lang/String;)V +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestFormParams : io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestFormParams$Companion; + public synthetic fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata; + public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema; + public final fun component4-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun copy-6R815Ik (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestFormParams; + public static synthetic fun copy-6R815Ik$default (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestFormParams;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestFormParams; + public fun equals (Ljava/lang/Object;)Z + public fun getMessage ()Ljava/lang/String; + public fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun getMode ()Ljava/lang/String; + public final fun getRequestedSchema ()Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema; + public final fun getTask ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestFormParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestFormParams$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestFormParams; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestFormParams;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestFormParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams : io/modelcontextprotocol/kotlin/sdk/types/RequestParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$Companion; + public abstract fun getMessage ()Ljava/lang/String; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema$Companion; + public fun (Ljava/lang/String;Ljava/util/Map;Ljava/util/List;)V + public synthetic fun (Ljava/lang/String;Ljava/util/Map;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/util/Map; + public final fun component3 ()Ljava/util/List; + public final fun copy (Ljava/lang/String;Ljava/util/Map;Ljava/util/List;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Ljava/lang/String;Ljava/util/Map;Ljava/util/List;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema; + public fun equals (Ljava/lang/Object;)Z + public final fun getProperties ()Ljava/util/Map; + public final fun getRequired ()Ljava/util/List; + public final fun getSchema ()Ljava/lang/String; + public final fun getType ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestURLParams : io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestURLParams$Companion; + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata; + public final fun component5-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun copy-4asOmiE (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestURLParams; + public static synthetic fun copy-4asOmiE$default (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestURLParams;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestURLParams; + public fun equals (Ljava/lang/Object;)Z + public final fun getElicitationId ()Ljava/lang/String; + public fun getMessage ()Ljava/lang/String; + public fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun getMode ()Ljava/lang/String; + public final fun getTask ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata; + public final fun getUrl ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestURLParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestURLParams$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestURLParams; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestURLParams;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestURLParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestedSchemaBuilder { + public fun ()V + public final fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema; + public final fun getRequired ()Ljava/util/List; + public final fun properties (Lkotlin/jvm/functions/Function1;)V + public final fun properties (Lkotlinx/serialization/json/JsonObject;)V + public final fun setRequired (Ljava/util/List;)V +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitResult : io/modelcontextprotocol/kotlin/sdk/types/ClientResult { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult$Action;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult$Action;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult$Action; + public final fun component2 ()Lkotlinx/serialization/json/JsonObject; + public final fun component3 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult$Action;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult$Action;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult; + public fun equals (Ljava/lang/Object;)Z + public final fun getAction ()Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult$Action; + public final fun getContent ()Lkotlinx/serialization/json/JsonObject; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ElicitResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitResult$Action : java/lang/Enum { + public static final field Accept Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult$Action; + public static final field Cancel Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult$Action; + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult$Action$Companion; + public static final field Decline Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult$Action; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult$Action; + public static fun values ()[Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult$Action; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitResult$Action$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitResult$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotification : io/modelcontextprotocol/kotlin/sdk/types/ServerNotification { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotification$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotificationParams;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotificationParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotification;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotificationParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotification; + public fun equals (Ljava/lang/Object;)Z + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotificationParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/NotificationParams; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotification$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotification; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotification;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotification$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotificationParams : io/modelcontextprotocol/kotlin/sdk/types/NotificationParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotificationParams$Companion; + public fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotificationParams; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotificationParams;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotificationParams; + public fun equals (Ljava/lang/Object;)Z + public final fun getElicitationId ()Ljava/lang/String; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotificationParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotificationParams$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotificationParams; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotificationParams;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotificationParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitationKt { + public static final fun ElicitRequestParams-EDEVuBg (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestFormParams; + public static synthetic fun ElicitRequestParams-EDEVuBg$default (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestFormParams; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Elicitation_dslKt { + public static final fun buildElicitRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequest; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/EmbeddedResource : io/modelcontextprotocol/kotlin/sdk/types/ContentBlock { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/EmbeddedResource$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceContents;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceContents;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/ResourceContents; + public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/Annotations; + public final fun component3 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceContents;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/EmbeddedResource; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/EmbeddedResource;Lio/modelcontextprotocol/kotlin/sdk/types/ResourceContents;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/EmbeddedResource; + public fun equals (Ljava/lang/Object;)Z + public final fun getAnnotations ()Lio/modelcontextprotocol/kotlin/sdk/types/Annotations; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public final fun getResource ()Lio/modelcontextprotocol/kotlin/sdk/types/ResourceContents; + public fun getType ()Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/EmbeddedResource$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/EmbeddedResource$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/EmbeddedResource; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/EmbeddedResource;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/EmbeddedResource$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/EmptyResult : io/modelcontextprotocol/kotlin/sdk/types/ClientResult, io/modelcontextprotocol/kotlin/sdk/types/ServerResult { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/EmptyResult$Companion; + public fun ()V + public fun (Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/EmptyResult; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/EmptyResult;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/EmptyResult; + public fun equals (Ljava/lang/Object;)Z + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/EmptyResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/EmptyResult$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/EmptyResult; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/EmptyResult;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/EmptyResult$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/EnumOption { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/EnumOption$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/EnumOption; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/EnumOption;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/EnumOption; + public fun equals (Ljava/lang/Object;)Z + public final fun getConst ()Ljava/lang/String; + public final fun getTitle ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/EnumOption$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/EnumOption$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/EnumOption; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/EnumOption;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/EnumOption$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/EnumSchemaDefinition : io/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaDefinition { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/EnumSchemaDefinition$Companion; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/EnumSchemaDefinition$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/GetPromptRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequest$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequestParams;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequestParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequest; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequest;Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequest; + public fun equals (Ljava/lang/Object;)Z + public final fun getArguments ()Ljava/util/Map; + public final fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public final fun getName ()Ljava/lang/String; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequestParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/GetPromptRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequest$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequest; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequest;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/GetPromptRequest$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/GetPromptRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/RequestBuilder { + public fun ()V + public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequest; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; + public final fun getArguments ()Ljava/util/Map; + public final fun getName ()Ljava/lang/String; + public final fun setArguments (Ljava/util/Map;)V + public final fun setName (Ljava/lang/String;)V +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/GetPromptRequestParams : io/modelcontextprotocol/kotlin/sdk/types/RequestParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequestParams$Companion; + public synthetic fun (Ljava/lang/String;Ljava/util/Map;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/String;Ljava/util/Map;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/util/Map; + public final fun component3-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun copy-EDEVuBg (Ljava/lang/String;Ljava/util/Map;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequestParams; + public static synthetic fun copy-EDEVuBg$default (Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequestParams;Ljava/lang/String;Ljava/util/Map;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequestParams; + public fun equals (Ljava/lang/Object;)Z + public final fun getArguments ()Ljava/util/Map; + public fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun getName ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/GetPromptRequestParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequestParams$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequestParams; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequestParams;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/GetPromptRequestParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/GetPromptResult : io/modelcontextprotocol/kotlin/sdk/types/ServerResult { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptResult$Companion; + public fun (Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/List; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptResult; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptResult;Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptResult; + public fun equals (Ljava/lang/Object;)Z + public final fun getDescription ()Ljava/lang/String; + public final fun getMessages ()Ljava/util/List; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/GetPromptResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptResult$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptResult; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptResult;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/GetPromptResult$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest, io/modelcontextprotocol/kotlin/sdk/types/ServerRequest { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequest$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequestParams;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequestParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequest; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequest;Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequest; + public fun equals (Ljava/lang/Object;)Z + public final fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequestParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams; + public final fun getTaskId ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequest$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequest; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequest;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequest$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequestParams : io/modelcontextprotocol/kotlin/sdk/types/RequestParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequestParams$Companion; + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun copy-D-90kyg (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequestParams; + public static synthetic fun copy-D-90kyg$default (Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequestParams;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequestParams; + public fun equals (Ljava/lang/Object;)Z + public fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun getTaskId ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequestParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequestParams$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequestParams; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequestParams;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequestParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadResult : io/modelcontextprotocol/kotlin/sdk/types/ClientResult, io/modelcontextprotocol/kotlin/sdk/types/ServerResult { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadResult$Companion; + public static final synthetic fun box-impl (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadResult; + public static fun constructor-impl (Lkotlinx/serialization/json/JsonObject;)Lkotlinx/serialization/json/JsonObject; + public fun equals (Ljava/lang/Object;)Z + public static fun equals-impl (Lkotlinx/serialization/json/JsonObject;Ljava/lang/Object;)Z + public static final fun equals-impl0 (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Z + public static final fun get-impl (Lkotlinx/serialization/json/JsonObject;Ljava/lang/String;)Lkotlinx/serialization/json/JsonElement; + public final fun getJson ()Lkotlinx/serialization/json/JsonObject; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public static fun getMeta-impl (Lkotlinx/serialization/json/JsonObject;)Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public static fun hashCode-impl (Lkotlinx/serialization/json/JsonObject;)I + public fun toString ()Ljava/lang/String; + public static fun toString-impl (Lkotlinx/serialization/json/JsonObject;)Ljava/lang/String; + public final synthetic fun unbox-impl ()Lkotlinx/serialization/json/JsonObject; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadResult$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize-iveUaak (Lkotlinx/serialization/encoding/Decoder;)Lkotlinx/serialization/json/JsonObject; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize-kradpfw (Lkotlinx/serialization/encoding/Encoder;Lkotlinx/serialization/json/JsonObject;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadResult$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/GetTaskRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest, io/modelcontextprotocol/kotlin/sdk/types/ServerRequest { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequest$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequestParams;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequestParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequest; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequest;Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequest; + public fun equals (Ljava/lang/Object;)Z + public final fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequestParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams; + public final fun getTaskId ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/GetTaskRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequest$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequest; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequest;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/GetTaskRequest$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/GetTaskRequestParams : io/modelcontextprotocol/kotlin/sdk/types/RequestParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequestParams$Companion; + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun copy-D-90kyg (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequestParams; + public static synthetic fun copy-D-90kyg$default (Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequestParams;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequestParams; + public fun equals (Ljava/lang/Object;)Z + public fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun getTaskId ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/GetTaskRequestParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequestParams$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequestParams; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequestParams;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/GetTaskRequestParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/GetTaskResult : io/modelcontextprotocol/kotlin/sdk/types/ClientResult, io/modelcontextprotocol/kotlin/sdk/types/ServerResult, io/modelcontextprotocol/kotlin/sdk/types/TaskFields { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskResult$Companion; + public fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus; public final fun component3 ()Ljava/lang/String; public final fun component4 ()Ljava/lang/String; public final fun component5 ()Ljava/lang/String; public final fun component6 ()Ljava/lang/Long; - public final fun component7 ()Lio/modelcontextprotocol/kotlin/sdk/Annotations; - public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Lio/modelcontextprotocol/kotlin/sdk/Annotations;)Lio/modelcontextprotocol/kotlin/sdk/Resource; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/Resource;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Lio/modelcontextprotocol/kotlin/sdk/Annotations;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/Resource; + public final fun component7 ()Ljava/lang/Long; + public final fun component8 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskResult; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskResult;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskResult; public fun equals (Ljava/lang/Object;)Z - public final fun getAnnotations ()Lio/modelcontextprotocol/kotlin/sdk/Annotations; + public fun getCreatedAt ()Ljava/lang/String; + public fun getLastUpdatedAt ()Ljava/lang/String; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public fun getPollInterval ()Ljava/lang/Long; + public fun getStatus ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus; + public fun getStatusMessage ()Ljava/lang/String; + public fun getTaskId ()Ljava/lang/String; + public fun getTtl ()Ljava/lang/Long; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/GetTaskResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskResult$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskResult; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskResult;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/GetTaskResult$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Icon { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/Icon$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/Icon$Theme;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/Icon$Theme;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/util/List; + public final fun component4 ()Lio/modelcontextprotocol/kotlin/sdk/types/Icon$Theme; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/Icon$Theme;)Lio/modelcontextprotocol/kotlin/sdk/types/Icon; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/Icon;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/Icon$Theme;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/Icon; + public fun equals (Ljava/lang/Object;)Z + public final fun getMimeType ()Ljava/lang/String; + public final fun getSizes ()Ljava/util/List; + public final fun getSrc ()Ljava/lang/String; + public final fun getTheme ()Lio/modelcontextprotocol/kotlin/sdk/types/Icon$Theme; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/Icon$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/Icon$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/Icon; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/Icon;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Icon$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Icon$Theme : java/lang/Enum { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/Icon$Theme$Companion; + public static final field Dark Lio/modelcontextprotocol/kotlin/sdk/types/Icon$Theme; + public static final field Light Lio/modelcontextprotocol/kotlin/sdk/types/Icon$Theme; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/Icon$Theme; + public static fun values ()[Lio/modelcontextprotocol/kotlin/sdk/types/Icon$Theme; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Icon$Theme$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ImageContent : io/modelcontextprotocol/kotlin/sdk/types/MediaContent { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ImageContent$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/types/Annotations; + public final fun component4 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ImageContent; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ImageContent;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ImageContent; + public fun equals (Ljava/lang/Object;)Z + public final fun getAnnotations ()Lio/modelcontextprotocol/kotlin/sdk/types/Annotations; + public final fun getData ()Ljava/lang/String; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public final fun getMimeType ()Ljava/lang/String; + public fun getType ()Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ImageContent$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ImageContent$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ImageContent; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ImageContent;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ImageContent$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ImageContentBuilder : io/modelcontextprotocol/kotlin/sdk/types/MediaContentBuilder { + public fun ()V + public synthetic fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent; + public final fun getData ()Ljava/lang/String; + public final fun getMimeType ()Ljava/lang/String; + public final fun setData (Ljava/lang/String;)V + public final fun setMimeType (Ljava/lang/String;)V +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Implementation { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/Implementation$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/util/List; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)Lio/modelcontextprotocol/kotlin/sdk/types/Implementation; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/Implementation;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/Implementation; + public fun equals (Ljava/lang/Object;)Z + public final fun getIcons ()Ljava/util/List; + public final fun getName ()Ljava/lang/String; + public final fun getTitle ()Ljava/lang/String; + public final fun getVersion ()Ljava/lang/String; + public final fun getWebsiteUrl ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/Implementation$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/Implementation$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/Implementation; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/Implementation;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Implementation$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/IncludeContext : java/lang/Enum { + public static final field AllServers Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext; + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext$Companion; + public static final field None Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext; + public static final field ThisServer Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext; + public static fun values ()[Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/IncludeContext$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/InitializeRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/InitializeRequest$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/InitializeRequestParams;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/InitializeRequestParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/InitializeRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/InitializeRequest; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/InitializeRequest;Lio/modelcontextprotocol/kotlin/sdk/types/InitializeRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/InitializeRequest; + public fun equals (Ljava/lang/Object;)Z + public final fun getCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; + public final fun getClientInfo ()Lio/modelcontextprotocol/kotlin/sdk/types/Implementation; + public final fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/InitializeRequestParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams; + public final fun getProtocolVersion ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/InitializeRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/InitializeRequest$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/InitializeRequest; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/InitializeRequest;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/InitializeRequest$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/InitializeRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/RequestBuilder { + public fun ()V + public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/InitializeRequest; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; + public final fun capabilities (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;)V + public final fun capabilities (Lkotlin/jvm/functions/Function1;)V + public final fun getProtocolVersion ()Ljava/lang/String; + public final fun info (Lio/modelcontextprotocol/kotlin/sdk/types/Implementation;)V + public final fun info (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V + public static synthetic fun info$default (Lio/modelcontextprotocol/kotlin/sdk/types/InitializeRequestBuilder;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILjava/lang/Object;)V + public final fun setProtocolVersion (Ljava/lang/String;)V +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/InitializeRequestParams : io/modelcontextprotocol/kotlin/sdk/types/RequestParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/InitializeRequestParams$Companion; + public synthetic fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;Lio/modelcontextprotocol/kotlin/sdk/types/Implementation;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;Lio/modelcontextprotocol/kotlin/sdk/types/Implementation;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; + public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/types/Implementation; + public final fun component4-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun copy-6R815Ik (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;Lio/modelcontextprotocol/kotlin/sdk/types/Implementation;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/InitializeRequestParams; + public static synthetic fun copy-6R815Ik$default (Lio/modelcontextprotocol/kotlin/sdk/types/InitializeRequestParams;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;Lio/modelcontextprotocol/kotlin/sdk/types/Implementation;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/InitializeRequestParams; + public fun equals (Ljava/lang/Object;)Z + public final fun getCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; + public final fun getClientInfo ()Lio/modelcontextprotocol/kotlin/sdk/types/Implementation; + public fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun getProtocolVersion ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/InitializeRequestParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/InitializeRequestParams$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/InitializeRequestParams; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/InitializeRequestParams;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/InitializeRequestParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/InitializeResult : io/modelcontextprotocol/kotlin/sdk/types/ServerResult { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/InitializeResult$Companion; + public fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;Lio/modelcontextprotocol/kotlin/sdk/types/Implementation;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;Lio/modelcontextprotocol/kotlin/sdk/types/Implementation;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities; + public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/types/Implementation; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;Lio/modelcontextprotocol/kotlin/sdk/types/Implementation;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/InitializeResult; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/InitializeResult;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;Lio/modelcontextprotocol/kotlin/sdk/types/Implementation;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/InitializeResult; + public fun equals (Ljava/lang/Object;)Z + public final fun getCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities; + public final fun getInstructions ()Ljava/lang/String; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public final fun getProtocolVersion ()Ljava/lang/String; + public final fun getServerInfo ()Lio/modelcontextprotocol/kotlin/sdk/types/Implementation; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/InitializeResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/InitializeResult$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/InitializeResult; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/InitializeResult;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/InitializeResult$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Initialize_dslKt { + public static final fun buildInitializeRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/InitializeRequest; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/InitializedNotification : io/modelcontextprotocol/kotlin/sdk/types/ClientNotification { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/InitializedNotification$Companion; + public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/InitializedNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/InitializedNotification;Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/InitializedNotification; + public fun equals (Ljava/lang/Object;)Z + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/NotificationParams; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/InitializedNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/InitializedNotification$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/InitializedNotification; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/InitializedNotification;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/InitializedNotification$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/IntegerSchema : io/modelcontextprotocol/kotlin/sdk/types/NumberSchemaDefinition { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/IntegerSchema$Companion; + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/Integer; + public final fun component4 ()Ljava/lang/Integer; + public final fun component5 ()Ljava/lang/Integer; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;)Lio/modelcontextprotocol/kotlin/sdk/types/IntegerSchema; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/IntegerSchema;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/IntegerSchema; + public fun equals (Ljava/lang/Object;)Z + public final fun getDefault ()Ljava/lang/Integer; public final fun getDescription ()Ljava/lang/String; + public final fun getMaximum ()Ljava/lang/Integer; + public final fun getMinimum ()Ljava/lang/Integer; + public final fun getTitle ()Ljava/lang/String; + public final fun getType ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/IntegerSchema$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/IntegerSchema$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/IntegerSchema; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/IntegerSchema;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/IntegerSchema$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/JSONRPCEmptyMessage : io/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCEmptyMessage; + public fun equals (Ljava/lang/Object;)Z + public fun getJsonrpc ()Ljava/lang/String; + public fun hashCode ()I + public final fun serializer ()Lkotlinx/serialization/KSerializer; + public fun toString ()Ljava/lang/String; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/JSONRPCError : io/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCError$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Lio/modelcontextprotocol/kotlin/sdk/types/RPCError;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestId; + public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/RPCError; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Lio/modelcontextprotocol/kotlin/sdk/types/RPCError;)Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCError; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCError;Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Lio/modelcontextprotocol/kotlin/sdk/types/RPCError;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCError; + public fun equals (Ljava/lang/Object;)Z + public final fun getError ()Lio/modelcontextprotocol/kotlin/sdk/types/RPCError; + public final fun getId ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestId; + public fun getJsonrpc ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/JSONRPCError$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCError$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCError; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCError;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/JSONRPCError$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage$Companion; + public abstract fun getJsonrpc ()Ljava/lang/String; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/JSONRPCNotification : io/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCNotification$Companion; + public fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;)V + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Lkotlinx/serialization/json/JsonElement; + public final fun copy (Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;)Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCNotification;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCNotification; + public fun equals (Ljava/lang/Object;)Z + public fun getJsonrpc ()Ljava/lang/String; + public final fun getMethod ()Ljava/lang/String; + public final fun getParams ()Lkotlinx/serialization/json/JsonElement; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/JSONRPCNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCNotification$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCNotification; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCNotification;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/JSONRPCNotification$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/JSONRPCRequest : io/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCRequest$Companion; + public fun (JLjava/lang/String;Lkotlinx/serialization/json/JsonElement;)V + public synthetic fun (JLjava/lang/String;Lkotlinx/serialization/json/JsonElement;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;)V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;)V + public fun (Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestId; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Lkotlinx/serialization/json/JsonElement; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;)Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCRequest; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCRequest;Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCRequest; + public fun equals (Ljava/lang/Object;)Z + public final fun getId ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestId; + public fun getJsonrpc ()Ljava/lang/String; + public final fun getMethod ()Ljava/lang/String; + public final fun getParams ()Lkotlinx/serialization/json/JsonElement; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/JSONRPCRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCRequest$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCRequest; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCRequest;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/JSONRPCRequest$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/JSONRPCResponse : io/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCResponse$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Lio/modelcontextprotocol/kotlin/sdk/types/RequestResult;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Lio/modelcontextprotocol/kotlin/sdk/types/RequestResult;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestId; + public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestResult; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Lio/modelcontextprotocol/kotlin/sdk/types/RequestResult;)Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCResponse; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCResponse;Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Lio/modelcontextprotocol/kotlin/sdk/types/RequestResult;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCResponse; + public fun equals (Ljava/lang/Object;)Z + public final fun getId ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestId; + public fun getJsonrpc ()Ljava/lang/String; + public final fun getResult ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestResult; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/JSONRPCResponse$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCResponse$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCResponse; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCResponse;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/JSONRPCResponse$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/JsonRpcKt { + public static final field JSONRPC_VERSION Ljava/lang/String; + public static final fun RequestId (J)Lio/modelcontextprotocol/kotlin/sdk/types/RequestId; + public static final fun RequestId (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/RequestId; + public static final fun fromJSON (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCRequest;)Lio/modelcontextprotocol/kotlin/sdk/types/Request; + public static final fun toJSON (Lio/modelcontextprotocol/kotlin/sdk/types/Notification;)Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCNotification; + public static final fun toJSON (Lio/modelcontextprotocol/kotlin/sdk/types/Request;)Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCRequest; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/JsonUtilsKt { + public static final fun getEmptyJsonObject ()Lkotlinx/serialization/json/JsonObject; + public static final fun getMcpJson ()Lkotlinx/serialization/json/Json; + public static final fun toJson (Ljava/util/Map;)Ljava/util/Map; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/LegacyTitledEnumSchema : io/modelcontextprotocol/kotlin/sdk/types/EnumSchemaDefinition { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/LegacyTitledEnumSchema$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/util/List; + public final fun component4 ()Ljava/util/List; + public final fun component5 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/LegacyTitledEnumSchema; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/LegacyTitledEnumSchema;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/LegacyTitledEnumSchema; + public fun equals (Ljava/lang/Object;)Z + public final fun getDefault ()Ljava/lang/String; + public final fun getDescription ()Ljava/lang/String; + public final fun getEnumNames ()Ljava/util/List; + public final fun getEnumValues ()Ljava/util/List; + public final fun getTitle ()Ljava/lang/String; + public final fun getType ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/LegacyTitledEnumSchema$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/LegacyTitledEnumSchema$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/LegacyTitledEnumSchema; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/LegacyTitledEnumSchema;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/LegacyTitledEnumSchema$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListPromptsRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest, io/modelcontextprotocol/kotlin/sdk/types/PaginatedRequest { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ListPromptsRequest$Companion; + public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ListPromptsRequest; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ListPromptsRequest;Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ListPromptsRequest; + public fun equals (Ljava/lang/Object;)Z + public fun getCursor ()Ljava/lang/String; + public fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ListPromptsRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ListPromptsRequest$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ListPromptsRequest; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ListPromptsRequest;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListPromptsRequest$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListPromptsRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestBuilder { + public fun ()V + public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/ListPromptsRequest; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListPromptsResult : io/modelcontextprotocol/kotlin/sdk/types/PaginatedResult, io/modelcontextprotocol/kotlin/sdk/types/ServerResult { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ListPromptsResult$Companion; + public fun (Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/List; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ListPromptsResult; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ListPromptsResult;Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ListPromptsResult; + public fun equals (Ljava/lang/Object;)Z + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public fun getNextCursor ()Ljava/lang/String; + public final fun getPrompts ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ListPromptsResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ListPromptsResult$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ListPromptsResult; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ListPromptsResult;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListPromptsResult$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest, io/modelcontextprotocol/kotlin/sdk/types/PaginatedRequest { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesRequest$Companion; + public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesRequest; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesRequest;Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesRequest; + public fun equals (Ljava/lang/Object;)Z + public fun getCursor ()Ljava/lang/String; + public fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesRequest$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesRequest; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesRequest;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesRequest$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestBuilder { + public fun ()V + public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesRequest; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesResult : io/modelcontextprotocol/kotlin/sdk/types/PaginatedResult, io/modelcontextprotocol/kotlin/sdk/types/ServerResult { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesResult$Companion; + public fun (Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/List; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesResult; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesResult;Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesResult; + public fun equals (Ljava/lang/Object;)Z + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public fun getNextCursor ()Ljava/lang/String; + public final fun getResourceTemplates ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesResult$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesResult; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesResult;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesResult$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourcesRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest, io/modelcontextprotocol/kotlin/sdk/types/PaginatedRequest { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ListResourcesRequest$Companion; + public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ListResourcesRequest; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ListResourcesRequest;Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ListResourcesRequest; + public fun equals (Ljava/lang/Object;)Z + public fun getCursor ()Ljava/lang/String; + public fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ListResourcesRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ListResourcesRequest$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ListResourcesRequest; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ListResourcesRequest;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourcesRequest$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourcesRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestBuilder { + public fun ()V + public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/ListResourcesRequest; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourcesResult : io/modelcontextprotocol/kotlin/sdk/types/PaginatedResult, io/modelcontextprotocol/kotlin/sdk/types/ServerResult { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ListResourcesResult$Companion; + public fun (Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/List; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ListResourcesResult; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ListResourcesResult;Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ListResourcesResult; + public fun equals (Ljava/lang/Object;)Z + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public fun getNextCursor ()Ljava/lang/String; + public final fun getResources ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ListResourcesResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ListResourcesResult$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ListResourcesResult; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ListResourcesResult;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourcesResult$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest : io/modelcontextprotocol/kotlin/sdk/types/ServerRequest { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest$Companion; + public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest;Lio/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest; + public fun equals (Ljava/lang/Object;)Z + public final fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListRootsRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/RequestBuilder { + public fun ()V + public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListRootsResult : io/modelcontextprotocol/kotlin/sdk/types/ClientResult { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsResult$Companion; + public fun (Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/List; + public final fun component2 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsResult; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsResult;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsResult; + public fun equals (Ljava/lang/Object;)Z + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public final fun getRoots ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ListRootsResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsResult$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsResult; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsResult;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListRootsResult$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListTasksRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest, io/modelcontextprotocol/kotlin/sdk/types/PaginatedRequest, io/modelcontextprotocol/kotlin/sdk/types/ServerRequest { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ListTasksRequest$Companion; + public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ListTasksRequest; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ListTasksRequest;Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ListTasksRequest; + public fun equals (Ljava/lang/Object;)Z + public fun getCursor ()Ljava/lang/String; + public fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ListTasksRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ListTasksRequest$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ListTasksRequest; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ListTasksRequest;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListTasksRequest$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListTasksResult : io/modelcontextprotocol/kotlin/sdk/types/ClientResult, io/modelcontextprotocol/kotlin/sdk/types/PaginatedResult, io/modelcontextprotocol/kotlin/sdk/types/ServerResult { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ListTasksResult$Companion; + public fun (Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/List; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ListTasksResult; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ListTasksResult;Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ListTasksResult; + public fun equals (Ljava/lang/Object;)Z + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public fun getNextCursor ()Ljava/lang/String; + public final fun getTasks ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ListTasksResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ListTasksResult$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ListTasksResult; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ListTasksResult;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListTasksResult$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListToolsRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest, io/modelcontextprotocol/kotlin/sdk/types/PaginatedRequest { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsRequest$Companion; + public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsRequest; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsRequest;Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsRequest; + public fun equals (Ljava/lang/Object;)Z + public fun getCursor ()Ljava/lang/String; + public fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ListToolsRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsRequest$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsRequest; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsRequest;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListToolsRequest$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListToolsRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestBuilder { + public fun ()V + public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsRequest; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListToolsResult : io/modelcontextprotocol/kotlin/sdk/types/PaginatedResult, io/modelcontextprotocol/kotlin/sdk/types/ServerResult { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsResult$Companion; + public fun (Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/List; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsResult; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsResult;Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsResult; + public fun equals (Ljava/lang/Object;)Z + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public fun getNextCursor ()Ljava/lang/String; + public final fun getTools ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ListToolsResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsResult$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsResult; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsResult;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListToolsResult$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/LoggingLevel : java/lang/Enum { + public static final field Alert Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel$Companion; + public static final field Critical Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; + public static final field Debug Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; + public static final field Emergency Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; + public static final field Error Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; + public static final field Info Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; + public static final field Notice Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; + public static final field Warning Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; + public static fun values ()[Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/LoggingLevel$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotification : io/modelcontextprotocol/kotlin/sdk/types/ServerNotification { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotification$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotificationParams;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotificationParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotification;Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotificationParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotification; + public fun equals (Ljava/lang/Object;)Z + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotificationParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/NotificationParams; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotification$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotification; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotification;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotification$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotificationParams : io/modelcontextprotocol/kotlin/sdk/types/NotificationParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotificationParams$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel;Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel;Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; + public final fun component2 ()Lkotlinx/serialization/json/JsonElement; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel;Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotificationParams; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotificationParams;Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel;Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotificationParams; + public fun equals (Ljava/lang/Object;)Z + public final fun getData ()Lkotlinx/serialization/json/JsonElement; + public final fun getLevel ()Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; + public final fun getLogger ()Ljava/lang/String; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotificationParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotificationParams$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotificationParams; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotificationParams;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotificationParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Logging_dslKt { + public static final fun buildSetLevelRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequest; +} + +public abstract interface annotation class io/modelcontextprotocol/kotlin/sdk/types/McpDsl : java/lang/annotation/Annotation { +} + +public class io/modelcontextprotocol/kotlin/sdk/types/McpException : java/lang/Exception { + public fun (I)V + public fun (ILjava/lang/String;)V + public fun (ILjava/lang/String;Lkotlinx/serialization/json/JsonElement;)V + public fun (ILjava/lang/String;Lkotlinx/serialization/json/JsonElement;Ljava/lang/Throwable;)V + public synthetic fun (ILjava/lang/String;Lkotlinx/serialization/json/JsonElement;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getCode ()I + public final fun getData ()Lkotlinx/serialization/json/JsonElement; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/MediaContent : io/modelcontextprotocol/kotlin/sdk/types/ContentBlock, io/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent$Companion; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/MediaContent$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public abstract class io/modelcontextprotocol/kotlin/sdk/types/MediaContentBuilder { + public fun ()V + public final fun annotations (Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;)V + public final fun annotations (Ljava/util/List;Ljava/lang/Double;Ljava/lang/String;)V + public static synthetic fun annotations$default (Lio/modelcontextprotocol/kotlin/sdk/types/MediaContentBuilder;Ljava/util/List;Ljava/lang/Double;Ljava/lang/String;ILjava/lang/Object;)V + public abstract fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent; + protected final fun getAnnotations ()Lio/modelcontextprotocol/kotlin/sdk/types/Annotations; + protected final fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public final fun meta (Lkotlin/jvm/functions/Function1;)V + protected final fun setAnnotations (Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;)V + protected final fun setMeta (Lkotlinx/serialization/json/JsonObject;)V +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/Method { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/Method$Companion; + public abstract fun getValue ()Ljava/lang/String; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Method$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Method$Custom : io/modelcontextprotocol/kotlin/sdk/types/Method { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/Method$Custom$Companion; + public fun (Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/Method$Custom; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/Method$Custom;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/Method$Custom; + public fun equals (Ljava/lang/Object;)Z + public fun getValue ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/Method$Custom$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/Method$Custom$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/Method$Custom; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/Method$Custom;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Method$Custom$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Method$Defined : java/lang/Enum, io/modelcontextprotocol/kotlin/sdk/types/Method { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined$Companion; + public static final field CompletionComplete Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field ElicitationCreate Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field Initialize Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field LoggingSetLevel Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field NotificationsCancelled Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field NotificationsElicitationComplete Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field NotificationsInitialized Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field NotificationsMessage Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field NotificationsProgress Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field NotificationsPromptsListChanged Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field NotificationsResourcesListChanged Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field NotificationsResourcesUpdated Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field NotificationsRootsListChanged Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field NotificationsTasksStatus Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field NotificationsToolsListChanged Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field Ping Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field PromptsGet Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field PromptsList Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field ResourcesList Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field ResourcesRead Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field ResourcesSubscribe Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field ResourcesTemplatesList Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field ResourcesUnsubscribe Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field RootsList Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field SamplingCreateMessage Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field ServerDiscover Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field TasksCancel Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field TasksGet Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field TasksList Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field TasksResult Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field ToolsCall Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field ToolsList Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public fun getValue ()Ljava/lang/String; + public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static fun values ()[Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Method$Defined$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ModelHint { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ModelHint$Companion; + public fun (Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/ModelHint; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ModelHint;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ModelHint; + public fun equals (Ljava/lang/Object;)Z + public final fun getName ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ModelHint$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ModelHint$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ModelHint; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ModelHint;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ModelHint$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ModelPreferences { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences$Companion; + public fun ()V + public fun (Ljava/util/List;Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Double;)V + public synthetic fun (Ljava/util/List;Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Double;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/List; + public final fun component2 ()Ljava/lang/Double; + public final fun component3 ()Ljava/lang/Double; + public final fun component4 ()Ljava/lang/Double; + public final fun copy (Ljava/util/List;Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Double;)Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences;Ljava/util/List;Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Double;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences; + public fun equals (Ljava/lang/Object;)Z + public final fun getCostPriority ()Ljava/lang/Double; + public final fun getHints ()Ljava/util/List; + public final fun getIntelligencePriority ()Ljava/lang/Double; + public final fun getSpeedPriority ()Ljava/lang/Double; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ModelPreferences$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ModelPreferences$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/MultiSelectEnumSchema : io/modelcontextprotocol/kotlin/sdk/types/EnumSchemaDefinition { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/MultiSelectEnumSchema$Companion; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/MultiSelectEnumSchema$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/Notification { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/Notification$Companion; + public abstract fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public abstract fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/NotificationParams; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Notification$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/NotificationParams : io/modelcontextprotocol/kotlin/sdk/types/WithMeta { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/NotificationParams$Companion; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/NotificationParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/NumberSchemaDefinition : io/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaDefinition { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/NumberSchemaDefinition$Companion; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/NumberSchemaDefinition$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/PaginatedRequest : io/modelcontextprotocol/kotlin/sdk/types/Request { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequest$Companion; + public fun getCursor ()Ljava/lang/String; + public fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public abstract fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/PaginatedRequest$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/PaginatedRequest$DefaultImpls { + public static fun getCursor (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequest;)Ljava/lang/String; + public static fun getMeta-VI-3G7E (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequest;)Lkotlinx/serialization/json/JsonObject; +} + +public abstract class io/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/RequestBuilder { + public fun ()V + public final fun getCursor ()Ljava/lang/String; + public final fun setCursor (Ljava/lang/String;)V +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams : io/modelcontextprotocol/kotlin/sdk/types/RequestParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams$Companion; + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun copy-D-90kyg (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams; + public static synthetic fun copy-D-90kyg$default (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams; + public fun equals (Ljava/lang/Object;)Z + public final fun getCursor ()Ljava/lang/String; + public fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/PaginatedResult : io/modelcontextprotocol/kotlin/sdk/types/RequestResult { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedResult$Companion; + public abstract fun getNextCursor ()Ljava/lang/String; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/PaginatedResult$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/PingRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest, io/modelcontextprotocol/kotlin/sdk/types/ServerRequest { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/PingRequest$Companion; + public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/PingRequest; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/PingRequest;Lio/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/PingRequest; + public fun equals (Ljava/lang/Object;)Z + public final fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/PingRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/PingRequest$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/PingRequest; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/PingRequest;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/PingRequest$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/PingRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/RequestBuilder { + public fun ()V + public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/PingRequest; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/PingRequest_dslKt { + public static final fun buildPingRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/PingRequest; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaDefinition { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaDefinition$Companion; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaDefinition$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Progress { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/Progress$Companion; + public fun (DLjava/lang/Double;Ljava/lang/String;)V + public synthetic fun (DLjava/lang/Double;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getMessage ()Ljava/lang/String; + public final fun getProgress ()D + public final fun getTotal ()Ljava/lang/Double; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/Progress$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/Progress$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/Progress; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/Progress;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Progress$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ProgressNotification : io/modelcontextprotocol/kotlin/sdk/types/ClientNotification, io/modelcontextprotocol/kotlin/sdk/types/ServerNotification { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ProgressNotification$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ProgressNotificationParams;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/ProgressNotificationParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ProgressNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ProgressNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ProgressNotification;Lio/modelcontextprotocol/kotlin/sdk/types/ProgressNotificationParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ProgressNotification; + public fun equals (Ljava/lang/Object;)Z + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/NotificationParams; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/ProgressNotificationParams; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ProgressNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ProgressNotification$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ProgressNotification; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ProgressNotification;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ProgressNotification$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ProgressNotificationParams : io/modelcontextprotocol/kotlin/sdk/types/NotificationParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ProgressNotificationParams$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;DLjava/lang/Double;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;DLjava/lang/Double;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestId; + public final fun component2 ()D + public final fun component3 ()Ljava/lang/Double; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;DLjava/lang/Double;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ProgressNotificationParams; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ProgressNotificationParams;Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;DLjava/lang/Double;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ProgressNotificationParams; + public fun equals (Ljava/lang/Object;)Z + public final fun getMessage ()Ljava/lang/String; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public final fun getProgress ()D + public final fun getProgressToken ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestId; + public final fun getTotal ()Ljava/lang/Double; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ProgressNotificationParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ProgressNotificationParams$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ProgressNotificationParams; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ProgressNotificationParams;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ProgressNotificationParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Prompt : io/modelcontextprotocol/kotlin/sdk/types/WithMeta { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/Prompt$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/util/List; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/util/List; + public final fun component6 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/Prompt; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/Prompt;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/Prompt; + public fun equals (Ljava/lang/Object;)Z + public final fun getArguments ()Ljava/util/List; + public final fun getDescription ()Ljava/lang/String; + public final fun getIcons ()Ljava/util/List; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public final fun getName ()Ljava/lang/String; + public final fun getTitle ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/Prompt$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/Prompt$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/Prompt; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/Prompt;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Prompt$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/PromptArgument { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/PromptArgument$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/Boolean; + public final fun component4 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/PromptArgument; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/PromptArgument;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/PromptArgument; + public fun equals (Ljava/lang/Object;)Z + public final fun getDescription ()Ljava/lang/String; + public final fun getName ()Ljava/lang/String; + public final fun getRequired ()Ljava/lang/Boolean; + public final fun getTitle ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/PromptArgument$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/PromptArgument$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/PromptArgument; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/PromptArgument;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/PromptArgument$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/PromptListChangedNotification : io/modelcontextprotocol/kotlin/sdk/types/ServerNotification { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/PromptListChangedNotification$Companion; + public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/PromptListChangedNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/PromptListChangedNotification;Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/PromptListChangedNotification; + public fun equals (Ljava/lang/Object;)Z + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/NotificationParams; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/PromptListChangedNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/PromptListChangedNotification$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/PromptListChangedNotification; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/PromptListChangedNotification;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/PromptListChangedNotification$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/PromptMessage { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/PromptMessage$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/ContentBlock;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/Role; + public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/ContentBlock; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/ContentBlock;)Lio/modelcontextprotocol/kotlin/sdk/types/PromptMessage; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/PromptMessage;Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/ContentBlock;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/PromptMessage; + public fun equals (Ljava/lang/Object;)Z + public final fun getContent ()Lio/modelcontextprotocol/kotlin/sdk/types/ContentBlock; + public final fun getRole ()Lio/modelcontextprotocol/kotlin/sdk/types/Role; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/PromptMessage$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/PromptMessage$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/PromptMessage; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/PromptMessage;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/PromptMessage$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/PromptReference : io/modelcontextprotocol/kotlin/sdk/types/Reference { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/PromptReference$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/PromptReference; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/PromptReference;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/PromptReference; + public fun equals (Ljava/lang/Object;)Z + public final fun getName ()Ljava/lang/String; + public final fun getTitle ()Ljava/lang/String; + public fun getType ()Lio/modelcontextprotocol/kotlin/sdk/types/ReferenceType; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/PromptReference$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/PromptReference$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/PromptReference; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/PromptReference;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/PromptReference$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Prompts_dslKt { + public static final fun buildGetPromptRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequest; + public static final fun buildListPromptsRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/ListPromptsRequest; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/RPCError { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/RPCError$Companion; + public fun (ILjava/lang/String;Lkotlinx/serialization/json/JsonElement;)V + public synthetic fun (ILjava/lang/String;Lkotlinx/serialization/json/JsonElement;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()I + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Lkotlinx/serialization/json/JsonElement; + public final fun copy (ILjava/lang/String;Lkotlinx/serialization/json/JsonElement;)Lio/modelcontextprotocol/kotlin/sdk/types/RPCError; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/RPCError;ILjava/lang/String;Lkotlinx/serialization/json/JsonElement;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/RPCError; + public fun equals (Ljava/lang/Object;)Z + public final fun getCode ()I + public final fun getData ()Lkotlinx/serialization/json/JsonElement; + public final fun getMessage ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/RPCError$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/RPCError$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/RPCError; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/RPCError;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/RPCError$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/RPCError$ErrorCode { + public static final field CONNECTION_CLOSED I + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/RPCError$ErrorCode; + public static final field INTERNAL_ERROR I + public static final field INVALID_PARAMS I + public static final field INVALID_REQUEST I + public static final field METHOD_NOT_FOUND I + public static final field PARSE_ERROR I + public static final field REQUEST_TIMEOUT I + public static final field RESOURCE_NOT_FOUND I + public static final field UNSUPPORTED_PROTOCOL_VERSION I + public static final field URL_ELICITATION_REQUIRED I +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequest$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequestParams;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequestParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequest; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequest;Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequest; + public fun equals (Ljava/lang/Object;)Z + public final fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequestParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams; + public final fun getUri ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequest$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequest; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequest;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequest$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/RequestBuilder { + public fun ()V + public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequest; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; + public final fun getUri ()Ljava/lang/String; + public final fun setUri (Ljava/lang/String;)V +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequestParams : io/modelcontextprotocol/kotlin/sdk/types/RequestParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequestParams$Companion; + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun copy-D-90kyg (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequestParams; + public static synthetic fun copy-D-90kyg$default (Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequestParams;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequestParams; + public fun equals (Ljava/lang/Object;)Z + public fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun getUri ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequestParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequestParams$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequestParams; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequestParams;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequestParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ReadResourceResult : io/modelcontextprotocol/kotlin/sdk/types/ServerResult { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceResult$Companion; + public fun (Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/List; + public final fun component2 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceResult; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceResult;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceResult; + public fun equals (Ljava/lang/Object;)Z + public final fun getContents ()Ljava/util/List; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ReadResourceResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceResult$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceResult; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceResult;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ReadResourceResult$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/Reference { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/Reference$Companion; + public abstract fun getType ()Lio/modelcontextprotocol/kotlin/sdk/types/ReferenceType; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Reference$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ReferenceType : java/lang/Enum { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ReferenceType$Companion; + public static final field Prompt Lio/modelcontextprotocol/kotlin/sdk/types/ReferenceType; + public static final field ResourceTemplate Lio/modelcontextprotocol/kotlin/sdk/types/ReferenceType; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public final fun getValue ()Ljava/lang/String; + public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/ReferenceType; + public static fun values ()[Lio/modelcontextprotocol/kotlin/sdk/types/ReferenceType; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ReferenceType$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/RelatedTaskMetadata { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/RelatedTaskMetadata$Companion; + public fun (Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/RelatedTaskMetadata; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/RelatedTaskMetadata;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/RelatedTaskMetadata; + public fun equals (Ljava/lang/Object;)Z + public final fun getTaskId ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/RelatedTaskMetadata$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/RelatedTaskMetadata$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/RelatedTaskMetadata; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/RelatedTaskMetadata;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/RelatedTaskMetadata$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/Request { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/Request$Companion; + public abstract fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public abstract fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Request$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public abstract class io/modelcontextprotocol/kotlin/sdk/types/RequestBuilder { + public fun ()V + protected final fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun meta (Lkotlin/jvm/functions/Function1;)V + protected final fun setMeta-mJxasco (Lkotlinx/serialization/json/JsonObject;)V +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/RequestId { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/RequestId$Companion; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/RequestId$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/RequestId$NumberId : io/modelcontextprotocol/kotlin/sdk/types/RequestId { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/RequestId$NumberId$Companion; + public static final synthetic fun box-impl (J)Lio/modelcontextprotocol/kotlin/sdk/types/RequestId$NumberId; + public static fun constructor-impl (J)J + public fun equals (Ljava/lang/Object;)Z + public static fun equals-impl (JLjava/lang/Object;)Z + public static final fun equals-impl0 (JJ)Z + public final fun getValue ()J + public fun hashCode ()I + public static fun hashCode-impl (J)I + public fun toString ()Ljava/lang/String; + public static fun toString-impl (J)Ljava/lang/String; + public final synthetic fun unbox-impl ()J +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/RequestId$NumberId$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/RequestId$NumberId$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize-Nt6Vbyc (Lkotlinx/serialization/encoding/Decoder;)J + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize-T5IKJsA (Lkotlinx/serialization/encoding/Encoder;J)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/RequestId$NumberId$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/RequestId$StringId : io/modelcontextprotocol/kotlin/sdk/types/RequestId { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/RequestId$StringId$Companion; + public static final synthetic fun box-impl (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/RequestId$StringId; + public static fun constructor-impl (Ljava/lang/String;)Ljava/lang/String; + public fun equals (Ljava/lang/Object;)Z + public static fun equals-impl (Ljava/lang/String;Ljava/lang/Object;)Z + public static final fun equals-impl0 (Ljava/lang/String;Ljava/lang/String;)Z + public final fun getValue ()Ljava/lang/String; + public fun hashCode ()I + public static fun hashCode-impl (Ljava/lang/String;)I + public fun toString ()Ljava/lang/String; + public static fun toString-impl (Ljava/lang/String;)Ljava/lang/String; + public final synthetic fun unbox-impl ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/RequestId$StringId$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/RequestId$StringId$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize-QNUQp7g (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/String; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize-i2Jt6-s (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/String;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/RequestId$StringId$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/RequestMeta { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/RequestMeta$Companion; + public static final synthetic fun box-impl (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/RequestMeta; + public static fun constructor-impl (Lkotlinx/serialization/json/JsonObject;)Lkotlinx/serialization/json/JsonObject; + public fun equals (Ljava/lang/Object;)Z + public static fun equals-impl (Lkotlinx/serialization/json/JsonObject;Ljava/lang/Object;)Z + public static final fun equals-impl0 (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Z + public static final fun get-impl (Lkotlinx/serialization/json/JsonObject;Ljava/lang/String;)Lkotlinx/serialization/json/JsonElement; + public static final fun getClientCapabilities-impl (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; + public static final fun getClientInfo-impl (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/Implementation; + public final fun getJson ()Lkotlinx/serialization/json/JsonObject; + public static final fun getLogLevel-impl (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; + public static final fun getProgressToken-impl (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/RequestId; + public static final fun getProtocolVersion-impl (Lkotlinx/serialization/json/JsonObject;)Ljava/lang/String; + public static final fun getRelatedTask-impl (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/RelatedTaskMetadata; + public fun hashCode ()I + public static fun hashCode-impl (Lkotlinx/serialization/json/JsonObject;)I + public fun toString ()Ljava/lang/String; + public static fun toString-impl (Lkotlinx/serialization/json/JsonObject;)Ljava/lang/String; + public final synthetic fun unbox-impl ()Lkotlinx/serialization/json/JsonObject; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/RequestMeta$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/RequestMeta$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize-6WR2xV8 (Lkotlinx/serialization/encoding/Decoder;)Lkotlinx/serialization/json/JsonObject; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize-qqtNtwg (Lkotlinx/serialization/encoding/Encoder;Lkotlinx/serialization/json/JsonObject;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/RequestMeta$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/RequestMetaBuilder { + public final fun progressToken (I)V + public final fun progressToken (J)V + public final fun progressToken (Ljava/lang/String;)V + public final fun put (Ljava/lang/String;Ljava/lang/Number;)Lkotlinx/serialization/json/JsonElement; + public final fun put (Ljava/lang/String;Ljava/lang/String;)Lkotlinx/serialization/json/JsonElement; + public final fun put (Ljava/lang/String;Ljava/lang/Void;)Lkotlinx/serialization/json/JsonElement; + public final fun put (Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;)Lkotlinx/serialization/json/JsonElement; + public final fun put (Ljava/lang/String;Z)Lkotlinx/serialization/json/JsonElement; + public final fun putJsonArray (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lkotlinx/serialization/json/JsonElement; + public final fun putJsonObject (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lkotlinx/serialization/json/JsonElement; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/RequestParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams$Companion; + public abstract fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/RequestParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/RequestResult : io/modelcontextprotocol/kotlin/sdk/types/WithMeta { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/RequestResult$Companion; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/RequestResult$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Resource : io/modelcontextprotocol/kotlin/sdk/types/ResourceLike { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/Resource$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/Long; + public final fun component6 ()Ljava/lang/String; + public final fun component7 ()Lio/modelcontextprotocol/kotlin/sdk/types/Annotations; + public final fun component8 ()Ljava/util/List; + public final fun component9 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/Resource; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/Resource;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/Resource; + public fun equals (Ljava/lang/Object;)Z + public final fun getAnnotations ()Lio/modelcontextprotocol/kotlin/sdk/types/Annotations; + public final fun getDescription ()Ljava/lang/String; + public final fun getIcons ()Ljava/util/List; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public final fun getMimeType ()Ljava/lang/String; public final fun getName ()Ljava/lang/String; public final fun getSize ()Ljava/lang/Long; @@ -2243,106 +4213,134 @@ public final class io/modelcontextprotocol/kotlin/sdk/Resource { public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/Resource$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/Resource$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/Resource$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/Resource$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/Resource; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/Resource; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/Resource;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/Resource;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/Resource$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/Resource$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public abstract interface class io/modelcontextprotocol/kotlin/sdk/ResourceContents { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ResourceContents$Companion; +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/ResourceContents : io/modelcontextprotocol/kotlin/sdk/types/WithMeta { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ResourceContents$Companion; public abstract fun getMimeType ()Ljava/lang/String; public abstract fun getUri ()Ljava/lang/String; } -public final class io/modelcontextprotocol/kotlin/sdk/ResourceContents$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceContents$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification : io/modelcontextprotocol/kotlin/sdk/ServerNotification { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Companion; - public fun ()V - public fun (Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params;)Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification;Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification; +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/ResourceLike : io/modelcontextprotocol/kotlin/sdk/types/WithMeta { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ResourceLike$Companion; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceLike$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceLink : io/modelcontextprotocol/kotlin/sdk/types/ContentBlock, io/modelcontextprotocol/kotlin/sdk/types/ResourceLike { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ResourceLink$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/Long; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Ljava/util/List; + public final fun component7 ()Ljava/lang/String; + public final fun component8 ()Lio/modelcontextprotocol/kotlin/sdk/types/Annotations; + public final fun component9 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ResourceLink; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceLink;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ResourceLink; public fun equals (Ljava/lang/Object;)Z - public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; - public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params; + public final fun getAnnotations ()Lio/modelcontextprotocol/kotlin/sdk/types/Annotations; + public final fun getDescription ()Ljava/lang/String; + public final fun getIcons ()Ljava/util/List; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public final fun getMimeType ()Ljava/lang/String; + public final fun getName ()Ljava/lang/String; + public final fun getSize ()Ljava/lang/Long; + public final fun getTitle ()Ljava/lang/String; + public fun getType ()Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; + public final fun getUri ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ResourceLink$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ResourceLink$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ResourceLink; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ResourceLink;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceLink$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params : io/modelcontextprotocol/kotlin/sdk/NotificationParams { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params$Companion; +public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceListChangedNotification : io/modelcontextprotocol/kotlin/sdk/types/ServerNotification { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ResourceListChangedNotification$Companion; public fun ()V - public fun (Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ResourceListChangedNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceListChangedNotification;Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ResourceListChangedNotification; public fun equals (Ljava/lang/Object;)Z - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/NotificationParams; public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ResourceListChangedNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ResourceListChangedNotification$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ResourceListChangedNotification; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ResourceListChangedNotification;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceListChangedNotification$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ResourceTemplate { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ResourceTemplate$Companion; - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Annotations;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Annotations;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate : io/modelcontextprotocol/kotlin/sdk/types/WithMeta { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Ljava/lang/String; public final fun component3 ()Ljava/lang/String; public final fun component4 ()Ljava/lang/String; public final fun component5 ()Ljava/lang/String; - public final fun component6 ()Lio/modelcontextprotocol/kotlin/sdk/Annotations; - public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Annotations;)Lio/modelcontextprotocol/kotlin/sdk/ResourceTemplate; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ResourceTemplate;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Annotations;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ResourceTemplate; + public final fun component6 ()Lio/modelcontextprotocol/kotlin/sdk/types/Annotations; + public final fun component7 ()Ljava/util/List; + public final fun component8 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate; public fun equals (Ljava/lang/Object;)Z - public final fun getAnnotations ()Lio/modelcontextprotocol/kotlin/sdk/Annotations; + public final fun getAnnotations ()Lio/modelcontextprotocol/kotlin/sdk/types/Annotations; public final fun getDescription ()Ljava/lang/String; + public final fun getIcons ()Ljava/util/List; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public final fun getMimeType ()Ljava/lang/String; public final fun getName ()Ljava/lang/String; public final fun getTitle ()Ljava/lang/String; @@ -2351,313 +4349,341 @@ public final class io/modelcontextprotocol/kotlin/sdk/ResourceTemplate { public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ResourceTemplate$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ResourceTemplate$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ResourceTemplate; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ResourceTemplate;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ResourceTemplate$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ResourceTemplateReference : io/modelcontextprotocol/kotlin/sdk/Reference { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ResourceTemplateReference$Companion; - public static final field TYPE Ljava/lang/String; +public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceTemplateReference : io/modelcontextprotocol/kotlin/sdk/types/Reference { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplateReference$Companion; public fun (Ljava/lang/String;)V public final fun component1 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/ResourceTemplateReference; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ResourceTemplateReference;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ResourceTemplateReference; + public final fun copy (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplateReference; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplateReference;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplateReference; public fun equals (Ljava/lang/Object;)Z - public fun getType ()Ljava/lang/String; + public fun getType ()Lio/modelcontextprotocol/kotlin/sdk/types/ReferenceType; public final fun getUri ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ResourceTemplateReference$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ResourceTemplateReference$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ResourceTemplateReference$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplateReference$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ResourceTemplateReference; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplateReference; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ResourceTemplateReference;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplateReference;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ResourceTemplateReference$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceTemplateReference$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification : io/modelcontextprotocol/kotlin/sdk/ServerNotification { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Companion; - public fun (Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params;)Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification;Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification; +public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotification : io/modelcontextprotocol/kotlin/sdk/types/ServerNotification { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotification$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotificationParams;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotificationParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotification;Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotificationParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotification; public fun equals (Ljava/lang/Object;)Z - public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; - public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/NotificationParams; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotificationParams; public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotification$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotification; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotification;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotification$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params : io/modelcontextprotocol/kotlin/sdk/NotificationParams { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params$Companion; +public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotificationParams : io/modelcontextprotocol/kotlin/sdk/types/NotificationParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotificationParams$Companion; public fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params; + public final fun copy (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotificationParams; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotificationParams;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotificationParams; public fun equals (Ljava/lang/Object;)Z + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public final fun getUri ()Ljava/lang/String; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotificationParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotificationParams$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotificationParams; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotificationParams;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotificationParams$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/Role : java/lang/Enum { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Role$Companion; - public static final field assistant Lio/modelcontextprotocol/kotlin/sdk/Role; - public static final field user Lio/modelcontextprotocol/kotlin/sdk/Role; +public final class io/modelcontextprotocol/kotlin/sdk/types/Resources_dslKt { + public static final fun buildListResourceTemplatesRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesRequest; + public static final fun buildListResourcesRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/ListResourcesRequest; + public static final fun buildReadResourceRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequest; + public static final fun buildSubscribeRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest; + public static final fun buildUnsubscribeRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequest; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Role : java/lang/Enum { + public static final field Assistant Lio/modelcontextprotocol/kotlin/sdk/types/Role; + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/Role$Companion; + public static final field User Lio/modelcontextprotocol/kotlin/sdk/types/Role; public static fun getEntries ()Lkotlin/enums/EnumEntries; - public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/Role; - public static fun values ()[Lio/modelcontextprotocol/kotlin/sdk/Role; + public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/Role; + public static fun values ()[Lio/modelcontextprotocol/kotlin/sdk/types/Role; } -public final class io/modelcontextprotocol/kotlin/sdk/Role$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/Role$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/Root { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Root$Companion; - public fun (Ljava/lang/String;Ljava/lang/String;)V +public final class io/modelcontextprotocol/kotlin/sdk/types/Root : io/modelcontextprotocol/kotlin/sdk/types/WithMeta { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/Root$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/Root; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/Root;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/Root; + public final fun component3 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/Root; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/Root;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/Root; public fun equals (Ljava/lang/Object;)Z + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public final fun getName ()Ljava/lang/String; public final fun getUri ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/Root$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/Root$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/Root$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/Root$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/Root; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/Root; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/Root;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/Root;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/Root$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/Root$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification : io/modelcontextprotocol/kotlin/sdk/ClientNotification { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Companion; +public final class io/modelcontextprotocol/kotlin/sdk/types/RootsListChangedNotification : io/modelcontextprotocol/kotlin/sdk/types/ClientNotification { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/RootsListChangedNotification$Companion; public fun ()V - public fun (Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params;)Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification;Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/RootsListChangedNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/RootsListChangedNotification;Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/RootsListChangedNotification; public fun equals (Ljava/lang/Object;)Z - public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; - public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/NotificationParams; public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/RootsListChangedNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/RootsListChangedNotification$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/RootsListChangedNotification; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/RootsListChangedNotification;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/RootsListChangedNotification$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params : io/modelcontextprotocol/kotlin/sdk/NotificationParams { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params$Companion; - public fun ()V - public fun (Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params; - public fun equals (Ljava/lang/Object;)Z - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; +public final class io/modelcontextprotocol/kotlin/sdk/types/Roots_dslKt { + public static final fun buildListRootsRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/SamplingMessage { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/SamplingMessage$Companion; - public fun (Lio/modelcontextprotocol/kotlin/sdk/Role;Lio/modelcontextprotocol/kotlin/sdk/PromptMessageContentMultimodal;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/Role; - public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/PromptMessageContentMultimodal; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/Role;Lio/modelcontextprotocol/kotlin/sdk/PromptMessageContentMultimodal;)Lio/modelcontextprotocol/kotlin/sdk/SamplingMessage; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/SamplingMessage;Lio/modelcontextprotocol/kotlin/sdk/Role;Lio/modelcontextprotocol/kotlin/sdk/PromptMessageContentMultimodal;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/SamplingMessage; - public fun equals (Ljava/lang/Object;)Z - public final fun getContent ()Lio/modelcontextprotocol/kotlin/sdk/PromptMessageContentMultimodal; - public final fun getRole ()Lio/modelcontextprotocol/kotlin/sdk/Role; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/SamplingMessage$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/SamplingMessage$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/SamplingMessage; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/SamplingMessage;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/SamplingMessage$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ServerCapabilities { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Companion; - public fun ()V - public fun (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Prompts;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Tools;)V - public synthetic fun (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Prompts;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Tools;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lkotlinx/serialization/json/JsonObject; - public final fun component2 ()Lkotlinx/serialization/json/JsonObject; +public final class io/modelcontextprotocol/kotlin/sdk/types/SamplingMessage { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessage$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/Role; + public final fun component2 ()Ljava/util/List; public final fun component3 ()Lkotlinx/serialization/json/JsonObject; - public final fun component4 ()Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Prompts; - public final fun component5 ()Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Resources; - public final fun component6 ()Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Tools; - public final fun copy (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Prompts;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Tools;)Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Prompts;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Tools;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessage; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessage;Lio/modelcontextprotocol/kotlin/sdk/types/Role;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessage; public fun equals (Ljava/lang/Object;)Z - public final fun getExperimental ()Lkotlinx/serialization/json/JsonObject; - public final fun getLogging ()Lkotlinx/serialization/json/JsonObject; - public final fun getPrompts ()Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Prompts; - public final fun getResources ()Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Resources; - public final fun getSampling ()Lkotlinx/serialization/json/JsonObject; - public final fun getTools ()Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Tools; + public final fun getContent ()Ljava/util/List; + public final fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public final fun getRole ()Lio/modelcontextprotocol/kotlin/sdk/types/Role; public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ServerCapabilities$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/SamplingMessage$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessage$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessage; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessage;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/SamplingMessage$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Prompts { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Prompts$Companion; +public final class io/modelcontextprotocol/kotlin/sdk/types/SamplingMessageBuilder { + public fun ()V + public final fun assistant (Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;)V + public final fun build ()Ljava/util/List; + public final fun user (Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;)V +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent : io/modelcontextprotocol/kotlin/sdk/types/WithMeta { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent$Companion; + public abstract fun getType ()Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Sampling_dslKt { + public static final fun assistant (Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageBuilder;Lkotlin/jvm/functions/Function0;)V + public static final fun assistantAudio (Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageBuilder;Lkotlin/jvm/functions/Function1;)V + public static final fun assistantImage (Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageBuilder;Lkotlin/jvm/functions/Function1;)V + public static final fun assistantText (Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageBuilder;Lkotlin/jvm/functions/Function1;)V + public static final fun buildCreateMessageRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest; + public static final fun user (Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageBuilder;Lkotlin/jvm/functions/Function0;)V + public static final fun userAudio (Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageBuilder;Lkotlin/jvm/functions/Function1;)V + public static final fun userImage (Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageBuilder;Lkotlin/jvm/functions/Function1;)V + public static final fun userText (Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageBuilder;Lkotlin/jvm/functions/Function1;)V +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Companion; + public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools; + public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources; + public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts; + public final fun component4 ()Lkotlinx/serialization/json/JsonObject; + public final fun component5 ()Lkotlinx/serialization/json/JsonObject; + public final fun component6 ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks; + public final fun component7 ()Lkotlinx/serialization/json/JsonObject; + public final fun component8 ()Ljava/util/Map; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities; + public fun equals (Ljava/lang/Object;)Z + public final fun getCompletions ()Lkotlinx/serialization/json/JsonObject; + public final fun getExperimental ()Lkotlinx/serialization/json/JsonObject; + public final fun getExtensions ()Ljava/util/Map; + public final fun getLogging ()Lkotlinx/serialization/json/JsonObject; + public final fun getPrompts ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts; + public final fun getResources ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources; + public final fun getTasks ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks; + public final fun getTools ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Companion { + public final fun getCompletions ()Lkotlinx/serialization/json/JsonObject; + public final fun getLogging ()Lkotlinx/serialization/json/JsonObject; + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts$Companion; + public fun ()V public fun (Ljava/lang/Boolean;)V + public synthetic fun (Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/Boolean; - public final fun copy (Ljava/lang/Boolean;)Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Prompts; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Prompts;Ljava/lang/Boolean;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Prompts; + public final fun copy (Ljava/lang/Boolean;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Ljava/lang/Boolean;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts; public fun equals (Ljava/lang/Object;)Z public final fun getListChanged ()Ljava/lang/Boolean; public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Prompts$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Prompts$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Prompts; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Prompts;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Prompts$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Resources { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Resources$Companion; +public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources$Companion; + public fun ()V public fun (Ljava/lang/Boolean;Ljava/lang/Boolean;)V + public synthetic fun (Ljava/lang/Boolean;Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/Boolean; public final fun component2 ()Ljava/lang/Boolean; - public final fun copy (Ljava/lang/Boolean;Ljava/lang/Boolean;)Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Resources; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Resources;Ljava/lang/Boolean;Ljava/lang/Boolean;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Resources; + public final fun copy (Ljava/lang/Boolean;Ljava/lang/Boolean;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Ljava/lang/Boolean;Ljava/lang/Boolean;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources; public fun equals (Ljava/lang/Object;)Z public final fun getListChanged ()Ljava/lang/Boolean; public final fun getSubscribe ()Ljava/lang/Boolean; @@ -2665,102 +4691,250 @@ public final class io/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Resourc public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Resources$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Resources$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Resources; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Resources;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Resources$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Tools { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Tools$Companion; +public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Companion; + public fun ()V + public fun (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests;)V + public synthetic fun (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lkotlinx/serialization/json/JsonObject; + public final fun component2 ()Lkotlinx/serialization/json/JsonObject; + public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests; + public final fun copy (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks; + public fun equals (Ljava/lang/Object;)Z + public final fun getCancel ()Lkotlinx/serialization/json/JsonObject; + public final fun getList ()Lkotlinx/serialization/json/JsonObject; + public final fun getRequests ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Companion; + public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests; + public fun equals (Ljava/lang/Object;)Z + public final fun getTools ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools$Companion; + public fun ()V + public fun (Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools; + public fun equals (Ljava/lang/Object;)Z + public final fun getCall ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools$Companion; + public fun ()V public fun (Ljava/lang/Boolean;)V + public synthetic fun (Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/Boolean; - public final fun copy (Ljava/lang/Boolean;)Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Tools; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Tools;Ljava/lang/Boolean;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Tools; + public final fun copy (Ljava/lang/Boolean;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Ljava/lang/Boolean;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools; public fun equals (Ljava/lang/Object;)Z public final fun getListChanged ()Ljava/lang/Boolean; public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Tools$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Tools$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Tools; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Tools;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ServerCapabilities$Tools$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public abstract interface class io/modelcontextprotocol/kotlin/sdk/ServerNotification : io/modelcontextprotocol/kotlin/sdk/Notification { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ServerNotification$Companion; +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/ServerNotification : io/modelcontextprotocol/kotlin/sdk/types/Notification { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ServerNotification$Companion; } -public final class io/modelcontextprotocol/kotlin/sdk/ServerNotification$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/ServerNotification$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public abstract interface class io/modelcontextprotocol/kotlin/sdk/ServerRequest : io/modelcontextprotocol/kotlin/sdk/Request { +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/ServerRequest : io/modelcontextprotocol/kotlin/sdk/types/Request { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ServerRequest$Companion; } -public abstract interface class io/modelcontextprotocol/kotlin/sdk/ServerResult : io/modelcontextprotocol/kotlin/sdk/RequestResult { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ServerResult$Companion; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ServerResult$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/ServerRequest$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public abstract interface class io/modelcontextprotocol/kotlin/sdk/StopReason { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/StopReason$Companion; - public abstract fun getValue ()Ljava/lang/String; +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/ServerResult : io/modelcontextprotocol/kotlin/sdk/types/RequestResult { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ServerResult$Companion; } -public final class io/modelcontextprotocol/kotlin/sdk/StopReason$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/ServerResult$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/StopReason$EndTurn : io/modelcontextprotocol/kotlin/sdk/StopReason { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/StopReason$EndTurn; +public final class io/modelcontextprotocol/kotlin/sdk/types/SetLevelRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequest$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequestParams;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequestParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequest; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequest;Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequest; public fun equals (Ljava/lang/Object;)Z - public fun getValue ()Ljava/lang/String; + public final fun getLevel ()Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; + public final fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequestParams; public fun hashCode ()I - public final fun serializer ()Lkotlinx/serialization/KSerializer; public fun toString ()Ljava/lang/String; } -public final class io/modelcontextprotocol/kotlin/sdk/StopReason$MaxTokens : io/modelcontextprotocol/kotlin/sdk/StopReason { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/StopReason$MaxTokens; - public fun equals (Ljava/lang/Object;)Z - public fun getValue ()Ljava/lang/String; - public fun hashCode ()I +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/SetLevelRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequest$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequest; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequest;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/SetLevelRequest$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/SetLevelRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/RequestBuilder { + public fun ()V + public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequest; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; + public final fun getLoggingLevel ()Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; + public final fun setLoggingLevel (Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel;)V +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/SetLevelRequestParams : io/modelcontextprotocol/kotlin/sdk/types/RequestParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequestParams$Companion; + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; + public final fun component2-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun copy-D-90kyg (Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequestParams; + public static synthetic fun copy-D-90kyg$default (Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequestParams;Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequestParams; + public fun equals (Ljava/lang/Object;)Z + public final fun getLevel ()Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; + public fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final class io/modelcontextprotocol/kotlin/sdk/StopReason$Other : io/modelcontextprotocol/kotlin/sdk/StopReason { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/StopReason$Other$Companion; - public static final synthetic fun box-impl (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/StopReason$Other; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/SetLevelRequestParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequestParams$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequestParams; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequestParams;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/SetLevelRequestParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/SingleSelectEnumSchema : io/modelcontextprotocol/kotlin/sdk/types/EnumSchemaDefinition { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/SingleSelectEnumSchema$Companion; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/SingleSelectEnumSchema$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/StopReason { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/StopReason$Companion; + public static final synthetic fun box-impl (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/StopReason; public static fun constructor-impl (Ljava/lang/String;)Ljava/lang/String; public fun equals (Ljava/lang/Object;)Z public static fun equals-impl (Ljava/lang/String;Ljava/lang/Object;)Z public static final fun equals-impl0 (Ljava/lang/String;Ljava/lang/String;)Z - public fun getValue ()Ljava/lang/String; + public final fun getValue ()Ljava/lang/String; public fun hashCode ()I public static fun hashCode-impl (Ljava/lang/String;)I public fun toString ()Ljava/lang/String; @@ -2768,103 +4942,387 @@ public final class io/modelcontextprotocol/kotlin/sdk/StopReason$Other : io/mode public final synthetic fun unbox-impl ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/StopReason$Other$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/StopReason$Other$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/StopReason$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/StopReason$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun deserialize-vvjsQ-s (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/String; + public final fun deserialize-_6z6jHk (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/String; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public final fun serialize-AuY3eX0 (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/String;)V + public final fun serialize-OgWWgxw (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/String;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/StopReason$Other$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/StopReason$Companion { + public final fun getEndTurn--MeDoko ()Ljava/lang/String; + public final fun getMaxTokens--MeDoko ()Ljava/lang/String; + public final fun getStopSequence--MeDoko ()Ljava/lang/String; + public final fun getToolUse--MeDoko ()Ljava/lang/String; public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/StopReason$StopSequence : io/modelcontextprotocol/kotlin/sdk/StopReason { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/StopReason$StopSequence; - public fun equals (Ljava/lang/Object;)Z - public fun getValue ()Ljava/lang/String; - public fun hashCode ()I - public final fun serializer ()Lkotlinx/serialization/KSerializer; - public fun toString ()Ljava/lang/String; -} - -public final class io/modelcontextprotocol/kotlin/sdk/SubscribeRequest : io/modelcontextprotocol/kotlin/sdk/ClientRequest, io/modelcontextprotocol/kotlin/sdk/WithMeta { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/SubscribeRequest$Companion; - public fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/SubscribeRequest; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/SubscribeRequest;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/SubscribeRequest; - public fun equals (Ljava/lang/Object;)Z - public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public final fun getUri ()Ljava/lang/String; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/SubscribeRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/SubscribeRequest$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/SubscribeRequest; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/SubscribeRequest;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/SubscribeRequest$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/TextContent : io/modelcontextprotocol/kotlin/sdk/PromptMessageContentMultimodal { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/TextContent$Companion; - public static final field TYPE Ljava/lang/String; +public final class io/modelcontextprotocol/kotlin/sdk/types/StringSchema : io/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaDefinition { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/StringSchema$Companion; public fun ()V - public fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Annotations;)V - public synthetic fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Annotations;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Lio/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Lio/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/Annotations; - public final fun copy (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Annotations;)Lio/modelcontextprotocol/kotlin/sdk/TextContent; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/TextContent;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Annotations;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/TextContent; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/Integer; + public final fun component4 ()Ljava/lang/Integer; + public final fun component5 ()Lio/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat; + public final fun component6 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Lio/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/StringSchema; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/StringSchema;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Lio/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/StringSchema; public fun equals (Ljava/lang/Object;)Z - public final fun getAnnotations ()Lio/modelcontextprotocol/kotlin/sdk/Annotations; - public final fun getText ()Ljava/lang/String; - public fun getType ()Ljava/lang/String; + public final fun getDefault ()Ljava/lang/String; + public final fun getDescription ()Ljava/lang/String; + public final fun getFormat ()Lio/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat; + public final fun getMaxLength ()Ljava/lang/Integer; + public final fun getMinLength ()Ljava/lang/Integer; + public final fun getTitle ()Ljava/lang/String; + public final fun getType ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/TextContent$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/TextContent$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/StringSchema$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/StringSchema$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/TextContent; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/StringSchema; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/TextContent;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/StringSchema;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/TextContent$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/StringSchema$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/TextResourceContents : io/modelcontextprotocol/kotlin/sdk/ResourceContents { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/TextResourceContents$Companion; - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V +public final class io/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat : java/lang/Enum { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat$Companion; + public static final field Date Lio/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat; + public static final field DateTime Lio/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat; + public static final field Email Lio/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat; + public static final field Uri Lio/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat; + public static fun values ()[Lio/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequestParams;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequestParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest; + public fun equals (Ljava/lang/Object;)Z + public final fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequestParams; + public final fun getUri ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/SubscribeRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/RequestBuilder { + public fun ()V + public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; + public final fun getUri ()Ljava/lang/String; + public final fun setUri (Ljava/lang/String;)V +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/SubscribeRequestParams : io/modelcontextprotocol/kotlin/sdk/types/RequestParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequestParams$Companion; + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun copy-D-90kyg (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequestParams; + public static synthetic fun copy-D-90kyg$default (Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequestParams;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequestParams; + public fun equals (Ljava/lang/Object;)Z + public fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun getUri ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/SubscribeRequestParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequestParams$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequestParams; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequestParams;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/SubscribeRequestParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Task : io/modelcontextprotocol/kotlin/sdk/types/TaskFields { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/Task$Companion; + public fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;)V + public synthetic fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Ljava/lang/Long; + public final fun component7 ()Ljava/lang/Long; + public final fun copy (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;)Lio/modelcontextprotocol/kotlin/sdk/types/Task; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/Task;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/Task; + public fun equals (Ljava/lang/Object;)Z + public fun getCreatedAt ()Ljava/lang/String; + public fun getLastUpdatedAt ()Ljava/lang/String; + public fun getPollInterval ()Ljava/lang/Long; + public fun getStatus ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus; + public fun getStatusMessage ()Ljava/lang/String; + public fun getTaskId ()Ljava/lang/String; + public fun getTtl ()Ljava/lang/Long; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/Task$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/Task$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/Task; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/Task;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Task$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/TaskFields { + public abstract fun getCreatedAt ()Ljava/lang/String; + public abstract fun getLastUpdatedAt ()Ljava/lang/String; + public abstract fun getPollInterval ()Ljava/lang/Long; + public abstract fun getStatus ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus; + public abstract fun getStatusMessage ()Ljava/lang/String; + public abstract fun getTaskId ()Ljava/lang/String; + public abstract fun getTtl ()Ljava/lang/Long; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/TaskMetadata { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata$Companion; + public fun ()V + public fun (Ljava/lang/Long;)V + public synthetic fun (Ljava/lang/Long;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/Long; + public final fun copy (Ljava/lang/Long;)Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Ljava/lang/Long;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata; + public fun equals (Ljava/lang/Object;)Z + public final fun getTtl ()Ljava/lang/Long; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/TaskMetadata$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/TaskMetadata$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/TaskStatus : java/lang/Enum { + public static final field Cancelled Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus; + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus$Companion; + public static final field Completed Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus; + public static final field Failed Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus; + public static final field InputRequired Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus; + public static final field Working Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus; + public static fun values ()[Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/TaskStatus$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotification : io/modelcontextprotocol/kotlin/sdk/types/ClientNotification, io/modelcontextprotocol/kotlin/sdk/types/ServerNotification { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotification$Companion; + public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificationParams;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificationParams;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificationParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotification;Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificationParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotification; + public fun equals (Ljava/lang/Object;)Z + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/NotificationParams; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificationParams; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotification$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotification; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotification;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotification$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificationParams : io/modelcontextprotocol/kotlin/sdk/types/NotificationParams, io/modelcontextprotocol/kotlin/sdk/types/TaskFields { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificationParams$Companion; + public fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Ljava/lang/Long; + public final fun component7 ()Ljava/lang/Long; + public final fun component8 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificationParams; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificationParams;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificationParams; + public fun equals (Ljava/lang/Object;)Z + public fun getCreatedAt ()Ljava/lang/String; + public fun getLastUpdatedAt ()Ljava/lang/String; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public fun getPollInterval ()Ljava/lang/Long; + public fun getStatus ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus; + public fun getStatusMessage ()Ljava/lang/String; + public fun getTaskId ()Ljava/lang/String; + public fun getTtl ()Ljava/lang/Long; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificationParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificationParams$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificationParams; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificationParams;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificationParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/TaskSupport : java/lang/Enum { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/TaskSupport$Companion; + public static final field Forbidden Lio/modelcontextprotocol/kotlin/sdk/types/TaskSupport; + public static final field Optional Lio/modelcontextprotocol/kotlin/sdk/types/TaskSupport; + public static final field Required Lio/modelcontextprotocol/kotlin/sdk/types/TaskSupport; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/TaskSupport; + public static fun values ()[Lio/modelcontextprotocol/kotlin/sdk/types/TaskSupport; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/TaskSupport$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/TasksKt { + public static final field RELATED_TASK_META_KEY Ljava/lang/String; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/TextContent : io/modelcontextprotocol/kotlin/sdk/types/MediaContent { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/TextContent$Companion; + public fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/Annotations; + public final fun component3 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/TextContent; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/TextContent;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/TextContent; + public fun equals (Ljava/lang/Object;)Z + public final fun getAnnotations ()Lio/modelcontextprotocol/kotlin/sdk/types/Annotations; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public final fun getText ()Ljava/lang/String; + public fun getType ()Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/TextContent$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/TextContent$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/TextContent; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/TextContent;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/TextContent$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/TextContentBuilder : io/modelcontextprotocol/kotlin/sdk/types/MediaContentBuilder { + public fun ()V + public synthetic fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent; + public final fun getText ()Ljava/lang/String; + public final fun setText (Ljava/lang/String;)V +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/TextResourceContents : io/modelcontextprotocol/kotlin/sdk/types/ResourceContents { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/TextResourceContents$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Ljava/lang/String; public final fun component3 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/TextResourceContents; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/TextResourceContents;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/TextResourceContents; + public final fun component4 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/TextResourceContents; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/TextResourceContents;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/TextResourceContents; public fun equals (Ljava/lang/Object;)Z + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public fun getMimeType ()Ljava/lang/String; public final fun getText ()Ljava/lang/String; public fun getUri ()Ljava/lang/String; @@ -2872,124 +5330,169 @@ public final class io/modelcontextprotocol/kotlin/sdk/TextResourceContents : io/ public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/TextResourceContents$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/TextResourceContents$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/TextResourceContents$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/TextResourceContents$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/TextResourceContents; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/TextResourceContents; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/TextResourceContents;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/TextResourceContents;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/TextResourceContents$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/TextResourceContents$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/Tool { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Tool$Companion; - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;)V +public final class io/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema : io/modelcontextprotocol/kotlin/sdk/types/MultiSelectEnumSchema { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items;Ljava/util/List;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Ljava/lang/String; - public final fun component4 ()Lio/modelcontextprotocol/kotlin/sdk/Tool$Input; - public final fun component5 ()Lio/modelcontextprotocol/kotlin/sdk/Tool$Output; - public final fun component6 ()Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations; - public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;)Lio/modelcontextprotocol/kotlin/sdk/Tool; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/Tool;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/Tool; + public final fun component3 ()Ljava/lang/Integer; + public final fun component4 ()Ljava/lang/Integer; + public final fun component5 ()Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items; + public final fun component6 ()Ljava/util/List; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items;Ljava/util/List;)Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items;Ljava/util/List;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema; public fun equals (Ljava/lang/Object;)Z - public final fun getAnnotations ()Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations; + public final fun getDefault ()Ljava/util/List; public final fun getDescription ()Ljava/lang/String; - public final fun getInputSchema ()Lio/modelcontextprotocol/kotlin/sdk/Tool$Input; + public final fun getItems ()Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items; + public final fun getMaxItems ()Ljava/lang/Integer; + public final fun getMinItems ()Ljava/lang/Integer; + public final fun getTitle ()Ljava/lang/String; + public final fun getType ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items$Companion; + public fun (Ljava/util/List;)V + public final fun component1 ()Ljava/util/List; + public final fun copy (Ljava/util/List;)Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items;Ljava/util/List;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items; + public fun equals (Ljava/lang/Object;)Z + public final fun getAnyOf ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/TitledSingleSelectEnumSchema : io/modelcontextprotocol/kotlin/sdk/types/SingleSelectEnumSchema { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/TitledSingleSelectEnumSchema$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/util/List; + public final fun component4 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/TitledSingleSelectEnumSchema; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/TitledSingleSelectEnumSchema;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/TitledSingleSelectEnumSchema; + public fun equals (Ljava/lang/Object;)Z + public final fun getDefault ()Ljava/lang/String; + public final fun getDescription ()Ljava/lang/String; + public final fun getOneOf ()Ljava/util/List; + public final fun getTitle ()Ljava/lang/String; + public final fun getType ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/TitledSingleSelectEnumSchema$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/TitledSingleSelectEnumSchema$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/TitledSingleSelectEnumSchema; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/TitledSingleSelectEnumSchema;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/TitledSingleSelectEnumSchema$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Tool : io/modelcontextprotocol/kotlin/sdk/types/WithMeta { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/Tool$Companion; + public fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations; + public final fun component7 ()Ljava/util/List; + public final fun component8 ()Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution; + public final fun component9 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/Tool; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/Tool;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/Tool; + public fun equals (Ljava/lang/Object;)Z + public final fun getAnnotations ()Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations; + public final fun getDescription ()Ljava/lang/String; + public final fun getExecution ()Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution; + public final fun getIcons ()Ljava/util/List; + public final fun getInputSchema ()Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public final fun getName ()Ljava/lang/String; - public final fun getOutputSchema ()Lio/modelcontextprotocol/kotlin/sdk/Tool$Output; + public final fun getOutputSchema ()Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema; public final fun getTitle ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/Tool$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/Tool$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/Tool$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/Tool$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/Tool; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/Tool; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/Tool;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/Tool;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/Tool$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/Tool$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/Tool$Input { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Tool$Input$Companion; +public final class io/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations$Companion; public fun ()V - public fun (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;)V - public synthetic fun (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lkotlinx/serialization/json/JsonObject; - public final fun component2 ()Ljava/util/List; - public final fun copy (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;)Lio/modelcontextprotocol/kotlin/sdk/Tool$Input; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/Tool$Input; - public fun equals (Ljava/lang/Object;)Z - public final fun getProperties ()Lkotlinx/serialization/json/JsonObject; - public final fun getRequired ()Ljava/util/List; - public final fun getType ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/Tool$Input$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/Tool$Input$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/Tool$Input; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/Tool$Input$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/Tool$Output { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Tool$Output$Companion; - public fun ()V - public fun (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;)V - public synthetic fun (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lkotlinx/serialization/json/JsonObject; - public final fun component2 ()Ljava/util/List; - public final fun copy (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;)Lio/modelcontextprotocol/kotlin/sdk/Tool$Output; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/Tool$Output; - public fun equals (Ljava/lang/Object;)Z - public final fun getProperties ()Lkotlinx/serialization/json/JsonObject; - public final fun getRequired ()Ljava/util/List; - public final fun getType ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/Tool$Output$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/Tool$Output$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/Tool$Output; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/Tool$Output$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ToolAnnotations { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations$Companion; public fun (Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;)V public synthetic fun (Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; @@ -2997,8 +5500,8 @@ public final class io/modelcontextprotocol/kotlin/sdk/ToolAnnotations { public final fun component3 ()Ljava/lang/Boolean; public final fun component4 ()Ljava/lang/Boolean; public final fun component5 ()Ljava/lang/Boolean; - public final fun copy (Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;)Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations; + public final fun copy (Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations; public fun equals (Ljava/lang/Object;)Z public final fun getDestructiveHint ()Ljava/lang/Boolean; public final fun getIdempotentHint ()Ljava/lang/Boolean; @@ -3009,356 +5512,550 @@ public final class io/modelcontextprotocol/kotlin/sdk/ToolAnnotations { public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ToolAnnotations$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ToolAnnotations$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification : io/modelcontextprotocol/kotlin/sdk/ServerNotification { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Companion; +public final class io/modelcontextprotocol/kotlin/sdk/types/ToolChoice { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Companion; public fun ()V - public fun (Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params;)Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification;Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice;Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice; public fun equals (Ljava/lang/Object;)Z - public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; - public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params; + public final fun getMode ()Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode; public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ToolChoice$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params : io/modelcontextprotocol/kotlin/sdk/NotificationParams { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params$Companion; +public final class io/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode : java/lang/Enum { + public static final field Auto Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode; + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode$Companion; + public static final field None Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode; + public static final field Required Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode; + public static fun values ()[Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ToolExecution { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution$Companion; public fun ()V - public fun (Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/TaskSupport;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/TaskSupport;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskSupport; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/TaskSupport;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution;Lio/modelcontextprotocol/kotlin/sdk/types/TaskSupport;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution; public fun equals (Ljava/lang/Object;)Z - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; + public final fun getTaskSupport ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskSupport; public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ToolExecution$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/ToolExecution$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/TypesKt { - public static final field JSONRPC_VERSION Ljava/lang/String; - public static final field LATEST_PROTOCOL_VERSION Ljava/lang/String; - public static final fun getSUPPORTED_PROTOCOL_VERSIONS ()[Ljava/lang/String; - public static final fun toJSON (Lio/modelcontextprotocol/kotlin/sdk/Notification;)Lio/modelcontextprotocol/kotlin/sdk/JSONRPCNotification; - public static final fun toJSON (Lio/modelcontextprotocol/kotlin/sdk/Request;)Lio/modelcontextprotocol/kotlin/sdk/JSONRPCRequest; +public final class io/modelcontextprotocol/kotlin/sdk/types/ToolListChangedNotification : io/modelcontextprotocol/kotlin/sdk/types/ServerNotification { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ToolListChangedNotification$Companion; + public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolListChangedNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ToolListChangedNotification;Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolListChangedNotification; + public fun equals (Ljava/lang/Object;)Z + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/NotificationParams; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } -public final class io/modelcontextprotocol/kotlin/sdk/Types_utilKt { - public static final fun error (Lio/modelcontextprotocol/kotlin/sdk/CallToolResult$Companion;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/CallToolResult; - public static synthetic fun error$default (Lio/modelcontextprotocol/kotlin/sdk/CallToolResult$Companion;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/CallToolResult; - public static final fun getEmptyJsonObject ()Lkotlinx/serialization/json/JsonObject; - public static final fun ok (Lio/modelcontextprotocol/kotlin/sdk/CallToolResult$Companion;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/CallToolResult; - public static synthetic fun ok$default (Lio/modelcontextprotocol/kotlin/sdk/CallToolResult$Companion;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/CallToolResult; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ToolListChangedNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ToolListChangedNotification$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolListChangedNotification; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ToolListChangedNotification;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/UnknownContent : io/modelcontextprotocol/kotlin/sdk/PromptMessageContentMultimodal { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/UnknownContent$Companion; - public fun (Ljava/lang/String;)V +public final class io/modelcontextprotocol/kotlin/sdk/types/ToolListChangedNotification$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ToolResultContent : io/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ToolResultContent$Companion; + public fun (Ljava/lang/String;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/lang/String;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/UnknownContent; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/UnknownContent;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/UnknownContent; + public final fun component2 ()Ljava/util/List; + public final fun component3 ()Lkotlinx/serialization/json/JsonObject; + public final fun component4 ()Ljava/lang/Boolean; + public final fun component5 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/lang/String;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolResultContent; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ToolResultContent;Ljava/lang/String;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolResultContent; public fun equals (Ljava/lang/Object;)Z - public fun getType ()Ljava/lang/String; + public final fun getContent ()Ljava/util/List; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public final fun getStructuredContent ()Lkotlinx/serialization/json/JsonObject; + public final fun getToolUseId ()Ljava/lang/String; + public fun getType ()Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; public fun hashCode ()I + public final fun isError ()Ljava/lang/Boolean; public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/UnknownContent$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/UnknownContent$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ToolResultContent$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ToolResultContent$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/UnknownContent; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolResultContent; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/UnknownContent;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ToolResultContent;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/UnknownContent$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/ToolResultContent$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification : io/modelcontextprotocol/kotlin/sdk/ClientNotification, io/modelcontextprotocol/kotlin/sdk/ClientRequest, io/modelcontextprotocol/kotlin/sdk/ServerNotification, io/modelcontextprotocol/kotlin/sdk/ServerRequest { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification$Companion; - public fun (Lio/modelcontextprotocol/kotlin/sdk/Method;Lio/modelcontextprotocol/kotlin/sdk/NotificationParams;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/Method;Lio/modelcontextprotocol/kotlin/sdk/NotificationParams;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/Method;Lio/modelcontextprotocol/kotlin/sdk/NotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification;Lio/modelcontextprotocol/kotlin/sdk/Method;Lio/modelcontextprotocol/kotlin/sdk/NotificationParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification; - public fun equals (Ljava/lang/Object;)Z - public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/UnknownReference : io/modelcontextprotocol/kotlin/sdk/Reference { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/UnknownReference$Companion; - public fun (Ljava/lang/String;)V +public final class io/modelcontextprotocol/kotlin/sdk/types/ToolSchema { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema$Companion; + public fun ()V + public fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/UnknownReference; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/UnknownReference;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/UnknownReference; + public final fun component2 ()Lkotlinx/serialization/json/JsonObject; + public final fun component3 ()Ljava/util/List; + public final fun component4 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema; public fun equals (Ljava/lang/Object;)Z - public fun getType ()Ljava/lang/String; + public final fun getDefs ()Lkotlinx/serialization/json/JsonObject; + public final fun getProperties ()Lkotlinx/serialization/json/JsonObject; + public final fun getRequired ()Ljava/util/List; + public final fun getSchema ()Ljava/lang/String; + public final fun getType ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/UnknownReference$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/UnknownReference$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ToolSchema$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/UnknownReference; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/UnknownReference;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/UnknownReference$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/ToolSchema$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/UnknownResourceContents : io/modelcontextprotocol/kotlin/sdk/ResourceContents { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/UnknownResourceContents$Companion; - public fun (Ljava/lang/String;Ljava/lang/String;)V +public final class io/modelcontextprotocol/kotlin/sdk/types/ToolUseContent : io/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ToolUseContent$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/UnknownResourceContents; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/UnknownResourceContents;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/UnknownResourceContents; + public final fun component3 ()Lkotlinx/serialization/json/JsonObject; + public final fun component4 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolUseContent; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ToolUseContent;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolUseContent; public fun equals (Ljava/lang/Object;)Z + public final fun getId ()Ljava/lang/String; + public final fun getInput ()Lkotlinx/serialization/json/JsonObject; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public final fun getName ()Ljava/lang/String; + public fun getType ()Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ToolUseContent$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ToolUseContent$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolUseContent; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ToolUseContent;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ToolUseContent$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ToolsKt { + public static final fun error (Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult$Companion;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult; + public static synthetic fun error$default (Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult$Companion;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult; + public static final fun success (Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult$Companion;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult; + public static synthetic fun success$default (Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult$Companion;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/Tools_dslKt { + public static final fun buildCallToolRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequest; + public static final fun buildListToolsRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsRequest; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/UnknownResourceContents : io/modelcontextprotocol/kotlin/sdk/types/ResourceContents { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/UnknownResourceContents$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/UnknownResourceContents; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/UnknownResourceContents;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/UnknownResourceContents; + public fun equals (Ljava/lang/Object;)Z + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public fun getMimeType ()Ljava/lang/String; public fun getUri ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/UnknownResourceContents$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/UnknownResourceContents$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/UnknownResourceContents$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/UnknownResourceContents$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/UnknownResourceContents; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/UnknownResourceContents; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/UnknownResourceContents;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/UnknownResourceContents;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/UnknownResourceContents$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/UnknownResourceContents$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/UnsubscribeRequest : io/modelcontextprotocol/kotlin/sdk/ClientRequest, io/modelcontextprotocol/kotlin/sdk/WithMeta { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/UnsubscribeRequest$Companion; - public fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V +public final class io/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequest$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequestParams;)V public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/UnsubscribeRequest; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/UnsubscribeRequest;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/UnsubscribeRequest; + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequestParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequest; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequest; public fun equals (Ljava/lang/Object;)Z - public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; + public final fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequestParams; public final fun getUri ()Ljava/lang/String; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/UnsubscribeRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/UnsubscribeRequest$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequest$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/UnsubscribeRequest; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequest; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/UnsubscribeRequest;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequest;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/UnsubscribeRequest$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequest$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public abstract interface class io/modelcontextprotocol/kotlin/sdk/WithMeta { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/WithMeta$Companion; - public abstract fun get_meta ()Lkotlinx/serialization/json/JsonObject; -} - -public final class io/modelcontextprotocol/kotlin/sdk/WithMeta$Companion { - public final fun getEmpty ()Lio/modelcontextprotocol/kotlin/sdk/CustomMeta; - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/internal/Utils_jvmKt { - public static final fun getIODispatcher ()Lkotlinx/coroutines/CoroutineDispatcher; -} - -public abstract class io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport : io/modelcontextprotocol/kotlin/sdk/shared/Transport { +public final class io/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/RequestBuilder { public fun ()V - protected final fun get_onClose ()Lkotlin/jvm/functions/Function0; - protected final fun get_onError ()Lkotlin/jvm/functions/Function1; - protected final fun get_onMessage ()Lkotlin/jvm/functions/Function2; - public fun onClose (Lkotlin/jvm/functions/Function0;)V - public fun onError (Lkotlin/jvm/functions/Function1;)V - public fun onMessage (Lkotlin/jvm/functions/Function2;)V + public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequest; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; + public final fun getUri ()Ljava/lang/String; + public final fun setUri (Ljava/lang/String;)V } -public abstract class io/modelcontextprotocol/kotlin/sdk/shared/Protocol { - public fun (Lio/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptions;)V - protected abstract fun assertCapabilityForMethod (Lio/modelcontextprotocol/kotlin/sdk/Method;)V - protected abstract fun assertNotificationCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V - public abstract fun assertRequestHandlerCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V - public final fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun connect (Lio/modelcontextprotocol/kotlin/sdk/shared/Transport;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun getFallbackNotificationHandler ()Lkotlin/jvm/functions/Function2; - public final fun getFallbackRequestHandler ()Lkotlin/jvm/functions/Function3; - public final fun getNotificationHandlers ()Ljava/util/Map; - public final fun getOptions ()Lio/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptions; - public final fun getProgressHandlers ()Ljava/util/Map; - public final fun getRequestHandlers ()Ljava/util/Map; - public final fun getResponseHandlers ()Ljava/util/Map; - public final fun getTransport ()Lio/modelcontextprotocol/kotlin/sdk/shared/Transport; - public final fun notification (Lio/modelcontextprotocol/kotlin/sdk/Notification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun onClose ()V - public fun onError (Ljava/lang/Throwable;)V - public final fun removeNotificationHandler (Lio/modelcontextprotocol/kotlin/sdk/Method;)V - public final fun removeRequestHandler (Lio/modelcontextprotocol/kotlin/sdk/Method;)V - public final fun request (Lio/modelcontextprotocol/kotlin/sdk/Request;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun request$default (Lio/modelcontextprotocol/kotlin/sdk/shared/Protocol;Lio/modelcontextprotocol/kotlin/sdk/Request;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun setFallbackNotificationHandler (Lkotlin/jvm/functions/Function2;)V - public final fun setFallbackRequestHandler (Lkotlin/jvm/functions/Function3;)V - public final fun setNotificationHandler (Lio/modelcontextprotocol/kotlin/sdk/Method;Lkotlin/jvm/functions/Function1;)V - public final fun setRequestHandler (Lkotlin/reflect/KType;Lio/modelcontextprotocol/kotlin/sdk/Method;Lkotlin/jvm/functions/Function3;)V -} - -public final class io/modelcontextprotocol/kotlin/sdk/shared/ProtocolKt { - public static final field IMPLEMENTATION_NAME Ljava/lang/String; - public static final fun getDEFAULT_REQUEST_TIMEOUT ()J - public static final fun getMcpJson ()Lkotlinx/serialization/json/Json; -} - -public class io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptions { - public synthetic fun (ZJILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (ZJLkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getEnforceStrictCapabilities ()Z - public final fun getTimeout-UwyO8pc ()J - public final fun setEnforceStrictCapabilities (Z)V - public final fun setTimeout-LRDsOJo (J)V -} - -public final class io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer { - public fun ()V - public final fun append ([B)V - public final fun clear ()V - public final fun readMessage ()Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage; -} - -public final class io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferKt { - public static final fun serializeMessage (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;)Ljava/lang/String; -} - -public final class io/modelcontextprotocol/kotlin/sdk/shared/RequestHandlerExtra { - public fun ()V -} - -public final class io/modelcontextprotocol/kotlin/sdk/shared/RequestOptions { - public synthetic fun (Lkotlin/jvm/functions/Function1;JILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Lkotlin/jvm/functions/Function1;JLkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lkotlin/jvm/functions/Function1; - public final fun component2-UwyO8pc ()J - public final fun copy-HG0u8IE (Lkotlin/jvm/functions/Function1;J)Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions; - public static synthetic fun copy-HG0u8IE$default (Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/jvm/functions/Function1;JILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions; +public final class io/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequestParams : io/modelcontextprotocol/kotlin/sdk/types/RequestParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequestParams$Companion; + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun copy-D-90kyg (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequestParams; + public static synthetic fun copy-D-90kyg$default (Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequestParams;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequestParams; public fun equals (Ljava/lang/Object;)Z - public final fun getOnProgress ()Lkotlin/jvm/functions/Function1; - public final fun getTimeout-UwyO8pc ()J + public fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun getUri ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public abstract interface class io/modelcontextprotocol/kotlin/sdk/shared/Transport { - public abstract fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public abstract fun onClose (Lkotlin/jvm/functions/Function0;)V - public abstract fun onError (Lkotlin/jvm/functions/Function1;)V - public abstract fun onMessage (Lkotlin/jvm/functions/Function2;)V - public abstract fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public abstract fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequestParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequestParams$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequestParams; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequestParams;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public abstract class io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { - public fun ()V - public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - protected abstract fun getSession ()Lio/ktor/websocket/WebSocketSession; - protected abstract fun initializeSession (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +public final class io/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequestParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransportKt { - public static final field MCP_SUBPROTOCOL Ljava/lang/String; +public final class io/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData$Companion; + public fun (Ljava/util/List;Ljava/lang/String;)V + public final fun component1 ()Ljava/util/List; + public final fun component2 ()Ljava/lang/String; + public final fun copy (Ljava/util/List;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData;Ljava/util/List;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData; + public fun equals (Ljava/lang/Object;)Z + public final fun getRequested ()Ljava/lang/String; + public final fun getSupported ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema : io/modelcontextprotocol/kotlin/sdk/types/MultiSelectEnumSchema { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items;Ljava/util/List;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/Integer; + public final fun component4 ()Ljava/lang/Integer; + public final fun component5 ()Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items; + public final fun component6 ()Ljava/util/List; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items;Ljava/util/List;)Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items;Ljava/util/List;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema; + public fun equals (Ljava/lang/Object;)Z + public final fun getDefault ()Ljava/util/List; + public final fun getDescription ()Ljava/lang/String; + public final fun getItems ()Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items; + public final fun getMaxItems ()Ljava/lang/Integer; + public final fun getMinItems ()Ljava/lang/Integer; + public final fun getTitle ()Ljava/lang/String; + public final fun getType ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items$Companion; + public fun (Ljava/util/List;)V + public final fun component1 ()Ljava/util/List; + public final fun copy (Ljava/util/List;)Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items;Ljava/util/List;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items; + public fun equals (Ljava/lang/Object;)Z + public final fun getEnumValues ()Ljava/util/List; + public final fun getType ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/UntitledSingleSelectEnumSchema : io/modelcontextprotocol/kotlin/sdk/types/SingleSelectEnumSchema { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/UntitledSingleSelectEnumSchema$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/util/List; + public final fun component4 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/UntitledSingleSelectEnumSchema; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/UntitledSingleSelectEnumSchema;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/UntitledSingleSelectEnumSchema; + public fun equals (Ljava/lang/Object;)Z + public final fun getDefault ()Ljava/lang/String; + public final fun getDescription ()Ljava/lang/String; + public final fun getEnumValues ()Ljava/util/List; + public final fun getTitle ()Ljava/lang/String; + public final fun getType ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/UntitledSingleSelectEnumSchema$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/UntitledSingleSelectEnumSchema$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/UntitledSingleSelectEnumSchema; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/UntitledSingleSelectEnumSchema;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/UntitledSingleSelectEnumSchema$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredData { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredData$Companion; + public fun (Ljava/util/List;)V + public final fun component1 ()Ljava/util/List; + public final fun copy (Ljava/util/List;)Lio/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredData; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredData;Ljava/util/List;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredData; + public fun equals (Ljava/lang/Object;)Z + public final fun getElicitations ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredData$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredData$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredData; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredData;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredData$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredException : io/modelcontextprotocol/kotlin/sdk/types/McpException { + public fun (Ljava/util/List;Ljava/lang/String;)V + public synthetic fun (Ljava/util/List;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getElicitations ()Ljava/util/List; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/WithMeta { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/WithMeta$Companion; + public abstract fun getMeta ()Lkotlinx/serialization/json/JsonObject; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/WithMeta$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/utils/MatchResult { + public fun (Ljava/util/Map;I)V + public fun equals (Ljava/lang/Object;)Z + public final fun getScore ()I + public final fun getVariables ()Ljava/util/Map; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcher : io/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcher { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcher$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate;)V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate;I)V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate;II)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate;IIILkotlin/jvm/internal/DefaultConstructorMarker;)V + public static final fun getFactory ()Lio/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcherFactory; + public fun getResourceTemplate ()Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate; + public fun match (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/utils/MatchResult; +} + +public final class io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcher$Companion { + public final fun getFactory ()Lio/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcherFactory; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcher { + public abstract fun getResourceTemplate ()Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate; + public abstract fun match (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/utils/MatchResult; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcherFactory { + public abstract fun create (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate;)Lio/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcher; } diff --git a/kotlin-sdk-core/build.gradle.kts b/kotlin-sdk-core/build.gradle.kts index 849c841..1d19f5d 100644 --- a/kotlin-sdk-core/build.gradle.kts +++ b/kotlin-sdk-core/build.gradle.kts @@ -4,6 +4,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl plugins { id("mcp.multiplatform") + id("mcp.detekt") id("mcp.publishing") id("mcp.dokka") alias(libs.plugins.kotlinx.binary.compatibility.validator) @@ -14,6 +15,9 @@ val generateLibVersion by tasks.registering { val outputDir = layout.buildDirectory.dir("generated-sources/libVersion") outputs.dir(outputDir) + // Capture version at configuration time to avoid configuration cache issues + val projectVersion = project.version.toString() + doLast { val sourceFile = outputDir.get().file("io/modelcontextprotocol/kotlin/sdk/LibVersion.kt").asFile sourceFile.parentFile.mkdirs() @@ -21,7 +25,7 @@ val generateLibVersion by tasks.registering { """ package io.modelcontextprotocol.kotlin.sdk - public const val LIB_VERSION: String = "${project.version}" + public const val LIB_VERSION: String = "$projectVersion" """.trimIndent(), ) @@ -32,10 +36,8 @@ kotlin { iosArm64() iosX64() iosSimulatorArm64() - watchosX64() watchosArm64() watchosSimulatorArm64() - tvosX64() tvosArm64() tvosSimulatorArm64() js { @@ -54,21 +56,23 @@ kotlin { api(libs.kotlinx.serialization.json) api(libs.kotlinx.coroutines.core) api(libs.kotlinx.io.core) - api(libs.ktor.server.websockets) api(libs.kotlinx.collections.immutable) + implementation(libs.ktor.server.websockets) implementation(libs.kotlin.logging) } } commonTest { dependencies { + implementation(project(":test-utils")) implementation(kotlin("test")) - implementation(libs.kotest.assertions.json) } } jvmTest { dependencies { + implementation(libs.junit.jupiter.params) + implementation(libs.mockk) runtimeOnly(libs.slf4j.simple) } } diff --git a/kotlin-sdk-core/detekt-baseline-main.xml b/kotlin-sdk-core/detekt-baseline-main.xml new file mode 100644 index 0000000..d483d7e --- /dev/null +++ b/kotlin-sdk-core/detekt-baseline-main.xml @@ -0,0 +1,7 @@ + + + + + UnsafeCallOnNullableType:Protocol.kt:Protocol$response!! + + diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/ExperimentalMcpApi.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/ExperimentalMcpApi.kt new file mode 100644 index 0000000..aa8f179 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/ExperimentalMcpApi.kt @@ -0,0 +1,57 @@ +package io.modelcontextprotocol.kotlin.sdk + +import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS +import kotlin.annotation.AnnotationTarget.CLASS +import kotlin.annotation.AnnotationTarget.CONSTRUCTOR +import kotlin.annotation.AnnotationTarget.FIELD +import kotlin.annotation.AnnotationTarget.FUNCTION +import kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE +import kotlin.annotation.AnnotationTarget.PROPERTY +import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER +import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER +import kotlin.annotation.AnnotationTarget.TYPEALIAS +import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER + +/** + * Annotation marking an API as experimental and subject to changes or removal in the future. + * + * This annotation is used to signal that a particular element, such as a class, function, or property, + * is part of an experimental API. Such APIs may not be stable, and their usage requires opting in + * explicitly. + * + * Users of the annotated API must explicitly accept the opt-in requirement to ensure they are aware + * of the potential instability or unfinished nature of the API. + * + * Targets that can be annotated include: + * - Classes + * - Annotation classes + * - Properties + * - Fields + * - Local variables + * - Value parameters + * - Constructors + * - Functions + * - Property getters + * - Property setters + * - Type aliases + */ +@RequiresOptIn( + message = "This API is experimental. It may be changed in the future without notice.", + level = RequiresOptIn.Level.WARNING, +) +@MustBeDocumented +@Target( + CLASS, + ANNOTATION_CLASS, + PROPERTY, + FIELD, + LOCAL_VARIABLE, + VALUE_PARAMETER, + CONSTRUCTOR, + FUNCTION, + PROPERTY_GETTER, + PROPERTY_SETTER, + TYPEALIAS, +) +@Retention(AnnotationRetention.BINARY) +public annotation class ExperimentalMcpApi diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/InternalMcpApi.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/InternalMcpApi.kt new file mode 100644 index 0000000..ba9b348 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/InternalMcpApi.kt @@ -0,0 +1,49 @@ +package io.modelcontextprotocol.kotlin.sdk + +import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS +import kotlin.annotation.AnnotationTarget.CLASS +import kotlin.annotation.AnnotationTarget.CONSTRUCTOR +import kotlin.annotation.AnnotationTarget.FIELD +import kotlin.annotation.AnnotationTarget.FUNCTION +import kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE +import kotlin.annotation.AnnotationTarget.PROPERTY +import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER +import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER +import kotlin.annotation.AnnotationTarget.TYPEALIAS +import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER + +/** + * Denotes that the annotated API is internal to the MCP SDK and its usage is restricted to components + * within the SDK. It is not intended to be used in general application code or by external consumers. + * + * This annotation is used to communicate and enforce the limited visibility of certain APIs to prevent + * accidental usage outside their intended context. + * + * Applying this annotation requires consumers to explicitly opt-in, acknowledging the warning level + * associated with its use. + * + * This annotation can be used on classes, annotations, properties, fields, local variables, value + * parameters, constructors, functions, property accessors, and type aliases. + * + * The annotation is retained in the binary and must be documented. + */ +@RequiresOptIn( + message = "This API is internal, and its usage is restricted to internal components of the MCP SDK.", + level = RequiresOptIn.Level.WARNING, +) +@MustBeDocumented +@Target( + CLASS, + ANNOTATION_CLASS, + PROPERTY, + FIELD, + LOCAL_VARIABLE, + VALUE_PARAMETER, + CONSTRUCTOR, + FUNCTION, + PROPERTY_GETTER, + PROPERTY_SETTER, + TYPEALIAS, +) +@Retention(AnnotationRetention.BINARY) +public annotation class InternalMcpApi diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.kt index d1cbe78..37a57b7 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.kt @@ -2,4 +2,5 @@ package io.modelcontextprotocol.kotlin.sdk.internal import kotlinx.coroutines.CoroutineDispatcher +/** Platform-specific [CoroutineDispatcher] for I/O-bound operations. */ public expect val IODispatcher: CoroutineDispatcher diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransport.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransport.kt new file mode 100644 index 0000000..27013d3 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransport.kt @@ -0,0 +1,276 @@ +package io.modelcontextprotocol.kotlin.sdk.shared + +import io.github.oshai.kotlinlogging.KLogger +import io.modelcontextprotocol.kotlin.sdk.InternalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.CONNECTION_CLOSED +import io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.INTERNAL_ERROR +import kotlin.concurrent.atomics.AtomicReference +import kotlin.concurrent.atomics.ExperimentalAtomicApi +import kotlin.coroutines.cancellation.CancellationException + +/** + * Abstract base class representing a client-side transport layer for communication. + * + * This class is responsible for managing the lifecycle of a transport instance, including its initialization, + * state transitions, message transmission, and graceful shutdown. It provides a framework for implementing + * specific transport mechanisms by defining abstract methods that subclasses must implement. + * + * Thread-safety is achieved through the use of atomic references to manage transport state transitions and + * to ensure invariants during lifecycle events. The initialization and shutdown logic is designed to handle + * async workflows, ensuring proper resource cleanup in the case of failure or cancellation. + */ +@OptIn(ExperimentalAtomicApi::class, InternalMcpApi::class) +public abstract class AbstractClientTransport : AbstractTransport() { + + protected abstract val logger: KLogger + + /** + * Represents the current state of the client transport in an atomic and thread-safe manner. + * + * This private field is used internally to manage state transitions of the transport, ensuring + * consistency across concurrent operations. The state transitions are validated and managed + * using the `stateTransition` method, adhering to defined rules in [ClientTransportState.VALID_TRANSITIONS]. + * + * @see ClientTransportState + * @see stateTransition + * @see checkState + */ + private val _state: AtomicReference = AtomicReference(ClientTransportState.New) + + /** + * Current transport state. Thread-safe via atomic reference. + * Initial state is [ClientTransportState.New]. + */ + @InternalMcpApi + protected val state: ClientTransportState + get() = _state.load() + + /** + * Updates the internal state of the client transport to the specified new state. + * + * This method is used to manage the lifecycle and operational state of the client transport + * by transitioning it to the provided [new] state. The new state is stored atomically, + * ensuring thread safety during concurrent modifications. + * + * @param new The new state to transition the client transport to. It must be a valid state + * as defined in the transport's lifecycle logic. + */ + @InternalMcpApi + protected fun updateState(new: ClientTransportState) { + _state.store(new) + } + + /** + * Performs transport-specific initialization (connections, I/O streams, background coroutines). + * + * Called by [start] during [ClientTransportState.Initializing]. On exception, state transitions to + * [ClientTransportState.InitializationFailed], [closeResources] is called, and exception propagates. + */ + protected abstract suspend fun initialize() + + /** + * Atomically transitions state from `from` to `to`. + * + * Validates transition per [ClientTransportState.VALID_TRANSITIONS] and performs atomic compare-and-exchange. + * + * @throws IllegalArgumentException if transition isn't allowed + * @throws IllegalStateException if the actual state doesn't match `from` + */ + @InternalMcpApi + protected fun stateTransition(from: ClientTransportState, to: ClientTransportState) { + require(to in ClientTransportState.VALID_TRANSITIONS.getValue(from)) { + "Invalid transition: $from → $to" + } + val actualState = _state.compareAndExchange( + expectedValue = from, + newValue = to, + ) + check(actualState == from) { + "Can't change state: expected transport state $from, but found $actualState." + } + } + + /** + * Verifies that the current transport state matches the expected state and throws an exception if it does not. + * + * This method ensures that the transport's state is in alignment with the caller’s expectations before + * proceeding with further operations. It is often used to enforce state invariants within the transport's + * lifecycle mechanics, enabling robust error handling and debugging in case of unexpected state transitions. + * + * @param expected The expected transport state. + * An exception will be thrown if the actual state does not match this value. + * @param lazyMessage Function that builds the exception message from the actual state when the check fails. + * Defaults to a message that compares the expected state against the observed one. + */ + protected fun checkState( + expected: ClientTransportState, + lazyMessage: (ClientTransportState) -> Any = { + "Expected transport state $expected, but found $it" + }, + ) { + val actualState = state + check(actualState == expected) { lazyMessage } + } + + /** + * Initiates the transport by transitioning through the required states to prepare it for operation. + * + * This method transitions the transport state from `New` to `Initializing`, performs initialization + * logic specific to the transport implementation, and then transitions the state to `Operational` + * upon successful initialization. If an exception occurs during initialization, the state is transitioned + * to `InitializationFailed`, resources are cleaned up, and the exception is rethrown. + * + * This method operates within a suspend context to allow for asynchronous initialization tasks. + * + * **Important! This method will eventually become `final`. + * Please don't override it, or consistency guarantees might be lost. Override [initialize] instead.** + * + * @throws Exception if the initialization process fails and transfers state + * to [ClientTransportState.InitializationFailed]. + * @see initialize + */ + public override suspend fun start() { + stateTransition(from = ClientTransportState.New, to = ClientTransportState.Initializing) + try { + initialize() + stateTransition(from = ClientTransportState.Initializing, to = ClientTransportState.Operational) + } catch (e: Exception) { + _state.store(ClientTransportState.InitializationFailed) + closeResources() + throw e + } + } + + /** + * Sends a JSON-RPC message using the transport layer. + * + * This method ensures that the transport is operational before attempting to send the message. + * If the transport's state does not allow sending operations, an exception is thrown. + * It delegates the actual transmission to the abstract [performSend] method, which must be + * implemented by subclasses to handle the specifics of the transmission mechanism. Errors + * during the send operation are handled appropriately, and cancellation exceptions are always + * propagated. + * + * **Important! This method will eventually become `final`. + * Please don't override it, or consistency guarantees might be lost. Override [performSend] instead.** + * + * @param message The JSON-RPC message to be sent. Must adhere to the JSON-RPC 2.0 specification. + * @param options Optional parameters that influence how the message is transmitted, such as + * timeout specifications or message delivery guarantees. Can be null if no + * specific options are required. + * @throws McpException If the transport is not operational or other application-specific errors occur. + * @throws CancellationException If the coroutine is canceled. + * @see performSend + */ + public override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { + // fast path - state check to avoid nonsense operations + if (state != ClientTransportState.Operational) { + throw McpException( + code = CONNECTION_CLOSED, + message = "Transport is not ready", + ) + } + // limitation: state could still change at the time of use + try { + performSend(message, options) + } catch (e: CancellationException) { + throw e // Always propagate cancellation + } catch (e: Exception) { + _onError(e) + if (e is McpException) { + throw e + } else { + throw McpException( + code = INTERNAL_ERROR, + message = "Error while sending message: ${e.message}", + cause = e, + ) + } + } + } + + /** + * Transmits a JSON-RPC message via the underlying protocol. + * + * The method is responsible for transmitting a given JSON-RPC message to the intended destination. + * It is implemented by concrete subclasses of the transport to handle specific transmission + * mechanisms. + * + * Called by [send] after verifying transport [state]. + * + * @param message The JSON-RPC message to be sent. Must comply with the JSON-RPC 2.0 specification. + * @param options Optional parameters that influence how the message is transmitted, such as + * timeout settings or delivery guarantees. May be null if no specific options + * are required. + * @see send + */ + protected abstract suspend fun performSend(message: JSONRPCMessage, options: TransportSendOptions? = null) + + /** + * Closes the client transport gracefully, transitioning its state to a non-operational state. + * + * **Important! This method will eventually become `final`. + * Please don't override it, or consistency guarantees might be lost.** + * + * The method is designed to operate within a suspendable context, allowing support for asynchronous + * behavior during the resource cleanup and state transitions. + * + * Transitions [ClientTransportState.Operational] → [ClientTransportState.ShuttingDown] → [ClientTransportState.Stopped]. + * Calls [closeResources] then invokes [_onClose] callbacks exactly once. + * On [kotlin.coroutines.cancellation.CancellationException], transitions to [ClientTransportState.ShutdownFailed] and propagates. + * + * The method also handles unexpected exceptions gracefully during resource cleanup by transitioning + * the transport to the `ShutdownFailed` state. + * + * **Important! This method will eventually become `final`. + * Please don't override it, or consistency guarantees might be lost. Override [closeResources] instead.** + * + * @see closeResources + */ + public override suspend fun close() { + val performClose: Boolean + when (state) { + ClientTransportState.Operational -> { + // Only Operational state can transition to ShuttingDown + stateTransition(ClientTransportState.Operational, ClientTransportState.ShuttingDown) + performClose = true + } + + ClientTransportState.New -> { + // New state transitions directly to Stopped without any cleanup + stateTransition(ClientTransportState.New, ClientTransportState.Stopped) + performClose = false + } + + else -> { + // Already shutting down or in terminal state - idempotent, do nothing + performClose = false + } + } + if (performClose) { + try { + closeResources() + stateTransition(from = ClientTransportState.ShuttingDown, to = ClientTransportState.Stopped) + } catch (e: CancellationException) { + stateTransition(from = ClientTransportState.ShuttingDown, to = ClientTransportState.ShutdownFailed) + throw e // Always propagate cancellation + } catch (e: Exception) { + // Ignore errors during cleanup + logger.error(e) { "Error during transport shutdown" } + stateTransition(from = ClientTransportState.ShuttingDown, to = ClientTransportState.ShutdownFailed) + } finally { + invokeOnCloseCallback() + } + } + } + + /** + * Releases transport-specific resources (coroutines, connections, streams). + * + * Called by [close] during [ClientTransportState.ShuttingDown]. + * May be called even if [initialize] failed partially. + */ + protected abstract suspend fun closeResources() +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport.kt new file mode 100644 index 0000000..a738289 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport.kt @@ -0,0 +1,73 @@ +package io.modelcontextprotocol.kotlin.sdk.shared + +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import kotlinx.coroutines.CompletableDeferred +import kotlin.concurrent.atomics.AtomicBoolean +import kotlin.concurrent.atomics.ExperimentalAtomicApi + +/** + * Base class for [Transport] implementations that handles the bookkeeping for [onClose], [onError] + * and [onMessage] callback registration, exposing the composed callbacks to subclasses for invocation. + * + * The callbacks are composed: each call to [onClose], [onError], or [onMessage] chains the new block + * after any previously registered ones. + */ +@OptIn(ExperimentalAtomicApi::class) +public abstract class AbstractTransport : Transport { + private val onCloseCalled = AtomicBoolean(false) + private var _onClose: (() -> Unit) = {} + protected var _onError: ((Throwable) -> Unit) = {} + private set + + // to not skip messages + private val _onMessageInitialized = CompletableDeferred() + protected var _onMessage: (suspend ((JSONRPCMessage) -> Unit)) = { + _onMessageInitialized.await() + _onMessage.invoke(it) + } + private set + + override fun onClose(block: () -> Unit) { + val old = _onClose + _onClose = { + old() + block() + } + } + + override fun onError(block: (Throwable) -> Unit) { + val old = _onError + _onError = { e -> + old(e) + block(e) + } + } + + override fun onMessage(block: suspend (JSONRPCMessage) -> Unit) { + val old: suspend (JSONRPCMessage) -> Unit = when (_onMessageInitialized.isCompleted) { + true -> _onMessage + false -> { _ -> } + } + + _onMessage = { message -> + old(message) + block(message) + } + + _onMessageInitialized.complete(Unit) + } + + /** + * Invokes the `_onClose` callback if it has not been already triggered. + * + * This method ensures the `_onClose` callback is executed only once by utilizing + * an atomic flag (`onCloseCalled`). If the callback has already been executed, + * the method does nothing. Any exceptions thrown during the execution of the + * `_onClose` callback are caught and suppressed. + */ + protected fun invokeOnCloseCallback() { + if (onCloseCalled.compareAndSet(expectedValue = false, newValue = true)) { + runCatching { _onClose() } + } + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ClientTransportState.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ClientTransportState.kt new file mode 100644 index 0000000..4f14d03 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ClientTransportState.kt @@ -0,0 +1,79 @@ +package io.modelcontextprotocol.kotlin.sdk.shared + +/** + * States of a client transport within the protocol lifecycle + * + * These states are generally aligned with the corresponding phases + * defined under the MCP [Protocol Lifecycle](https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle). + */ +public enum class ClientTransportState { + + /** + * The transport has just been created. + * + * This is the Initial state + */ + New, + + /** + * Indicates that the protocol + * is in the [Initialization Phase](https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initialization). + */ + Initializing, + + /** + * Indicates that the initialization phase of the protocol lifecycle has failed. + * + * This state suggests that the transport encountered an error or issue during + * initialization, preventing it from successfully transitioning to a fully operational state. + */ + InitializationFailed, + + /** + * Represents the [operational phase](https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#operation) + * of the protocol lifecycle. + * + * During the operation phase, the client and server exchange messages according to the negotiated capabilities. + */ + Operational, + + /** + * Represents the shutting down phase of the protocol lifecycle. + * + * This state indicates that the transport is in the process of shutting down + * and transitioning to a non-operational state. + * + * During this phase, no new outgoing messages should be accepted. + */ + ShuttingDown, + + /** + * Indicates that the shutdown phase of the protocol lifecycle has failed. + * + * This state signifies that an error was encountered during the shutdown phase. + */ + ShutdownFailed, + + /** + * Indicates that the transport has fully stopped. + * + * This state represents the final phase of the lifecycle, where the transport is no longer + * operational and all cleanup routines or resource deallocations have been completed. + */ + Stopped, + + ; + + internal companion object { + val VALID_TRANSITIONS: Map> = mapOf( + New to setOf(Initializing, Stopped), + Initializing to setOf(Operational, InitializationFailed), + Operational to setOf(ShuttingDown), + ShuttingDown to setOf(Stopped, ShutdownFailed), + // Terminal states allow no transitions + InitializationFailed to emptySet(), + Stopped to emptySet(), + ShutdownFailed to emptySet(), + ) + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt index 675bfea..908e524 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt @@ -1,48 +1,67 @@ package io.modelcontextprotocol.kotlin.sdk.shared import io.github.oshai.kotlinlogging.KotlinLogging -import io.modelcontextprotocol.kotlin.sdk.CancelledNotification -import io.modelcontextprotocol.kotlin.sdk.EmptyRequestResult -import io.modelcontextprotocol.kotlin.sdk.ErrorCode -import io.modelcontextprotocol.kotlin.sdk.JSONRPCError -import io.modelcontextprotocol.kotlin.sdk.JSONRPCNotification -import io.modelcontextprotocol.kotlin.sdk.JSONRPCRequest -import io.modelcontextprotocol.kotlin.sdk.JSONRPCResponse -import io.modelcontextprotocol.kotlin.sdk.McpError -import io.modelcontextprotocol.kotlin.sdk.Method -import io.modelcontextprotocol.kotlin.sdk.Notification -import io.modelcontextprotocol.kotlin.sdk.PingRequest -import io.modelcontextprotocol.kotlin.sdk.Progress -import io.modelcontextprotocol.kotlin.sdk.ProgressNotification -import io.modelcontextprotocol.kotlin.sdk.Request -import io.modelcontextprotocol.kotlin.sdk.RequestId -import io.modelcontextprotocol.kotlin.sdk.RequestResult -import io.modelcontextprotocol.kotlin.sdk.fromJSON -import io.modelcontextprotocol.kotlin.sdk.toJSON +import io.modelcontextprotocol.kotlin.sdk.types.CancelledNotification +import io.modelcontextprotocol.kotlin.sdk.types.CancelledNotificationParams +import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCEmptyMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCError +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.McpJson +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.Notification +import io.modelcontextprotocol.kotlin.sdk.types.PingRequest +import io.modelcontextprotocol.kotlin.sdk.types.Progress +import io.modelcontextprotocol.kotlin.sdk.types.ProgressNotification +import io.modelcontextprotocol.kotlin.sdk.types.ProgressToken +import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import io.modelcontextprotocol.kotlin.sdk.types.Request +import io.modelcontextprotocol.kotlin.sdk.types.RequestId +import io.modelcontextprotocol.kotlin.sdk.types.RequestResult +import io.modelcontextprotocol.kotlin.sdk.types.fromJSON +import io.modelcontextprotocol.kotlin.sdk.types.toJSON import kotlinx.atomicfu.AtomicRef import kotlinx.atomicfu.atomic import kotlinx.atomicfu.getAndUpdate import kotlinx.atomicfu.update +import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.PersistentMap +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentMapOf import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Deferred -import kotlinx.coroutines.TimeoutCancellationException -import kotlinx.coroutines.withTimeout -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.json.ClassDiscriminatorMode -import kotlinx.serialization.json.Json +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.job +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull +import kotlinx.serialization.SerializationException import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.encodeToJsonElement -import kotlinx.serialization.serializer -import kotlin.reflect.KType -import kotlin.reflect.typeOf +import kotlin.coroutines.AbstractCoroutineContextElement +import kotlin.coroutines.ContinuationInterceptor +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.cancellation.CancellationException import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds -private val LOGGER = KotlinLogging.logger { } +private val logger = KotlinLogging.logger { } +/** Default implementation name used in MCP handshake. */ public const val IMPLEMENTATION_NAME: String = "mcp-ktor" /** @@ -50,35 +69,29 @@ public const val IMPLEMENTATION_NAME: String = "mcp-ktor" */ public typealias ProgressCallback = (Progress) -> Unit -@OptIn(ExperimentalSerializationApi::class) -public val McpJson: Json by lazy { - Json { - ignoreUnknownKeys = true - encodeDefaults = true - isLenient = true - classDiscriminatorMode = ClassDiscriminatorMode.NONE - explicitNulls = false - } -} - /** * Additional initialization options. + * + * [handlerCoroutineContext] is read once when [Protocol.connect] attaches a transport and stays + * fixed until the next [Protocol.connect]. + * + * @property enforceStrictCapabilities whether to restrict emitted requests to only those the remote + * side advertised support for. Local-side capability checks are unaffected. Defaults to `false` for + * backward compatibility with SDK versions that did not advertise capabilities correctly. + * @property timeout default timeout for outgoing requests + * @property handlerCoroutineContext coroutine context used to run inbound request and notification + * handlers once initialization has completed. Must contain a real dispatching + * [kotlin.coroutines.ContinuationInterceptor]. `Dispatchers.Unconfined` and unconfined test + * dispatchers are unsupported, since handler resumptions would then run on the transport read loop + * and reintroduce head-of-line blocking. Any [kotlinx.coroutines.Job] in this context is ignored. */ public open class ProtocolOptions( - /** - * Whether to restrict emitted requests to only those that the remote side has indicated - * that they can handle, through their advertised capabilities. - * - * Note that this DOES NOT affect checking of _local_ side capabilities, as it is - * considered a logic error to mis-specify those. - * - * Currently, this defaults to false, for backwards compatibility with SDK versions - * that did not advertise capabilities correctly. - * In the future, this will default to true. - */ public var enforceStrictCapabilities: Boolean = false, - public var timeout: Duration = DEFAULT_REQUEST_TIMEOUT, + public var handlerCoroutineContext: CoroutineContext = Dispatchers.Default, + // Internal handler-concurrency bounds, read once at connect and not part of the public surface. + internal var maxConcurrentHandlers: Int = DEFAULT_MAX_CONCURRENT_HANDLERS, + internal var maxInFlightHandlers: Int = DEFAULT_MAX_IN_FLIGHT_HANDLERS, ) /** @@ -86,43 +99,155 @@ public open class ProtocolOptions( */ public val DEFAULT_REQUEST_TIMEOUT: Duration = 60.seconds +/** Default cap on concurrently executing inbound handlers per connection. */ +internal const val DEFAULT_MAX_CONCURRENT_HANDLERS: Int = 64 + +/** Default cap on launched-but-not-completed inbound handlers per connection. */ +internal const val DEFAULT_MAX_IN_FLIGHT_HANDLERS: Int = 256 + /** * Options that can be given per request. + * + * @param relatedRequestId if present, used to indicate to the transport which incoming request to + * associate this outgoing message with. + * @param resumptionToken the resumption token used to continue long-running requests that were interrupted. + * Allows clients to reconnect and continue from where they left off, if supported by the transport. + * @param onResumptionToken callback invoked when the resumption token changes, if supported by the transport. + * Allows clients to persist the latest token for potential reconnection. + * @property onProgress callback for progress notifications. + * If set, requests progress notifications from the remote end (if supported); + * when progress notifications are received, this callback is invoked. + * @property timeout a timeout for this request. + * If exceeded, a [McpException] with code `RequestTimeout` is raised from [Protocol.request]. + * If not specified, [DEFAULT_REQUEST_TIMEOUT] is used. */ -public data class RequestOptions( - /** - * If set, requests progress notifications from the remote end (if supported). - * When progress notifications are received, this callback will be invoked. - */ - val onProgress: ProgressCallback? = null, +public class RequestOptions( + relatedRequestId: RequestId? = null, + resumptionToken: String? = null, + onResumptionToken: ((String) -> Unit)? = null, + public val onProgress: ProgressCallback? = null, + public val timeout: Duration = DEFAULT_REQUEST_TIMEOUT, +) : TransportSendOptions(relatedRequestId, resumptionToken, onResumptionToken) { + /** Destructuring component for [onProgress]. */ + public operator fun component4(): ProgressCallback? = onProgress - /** - * A timeout for this request. If exceeded, an McpError with code `RequestTimeout` - * will be raised from request(). - * - * If not specified, `DEFAULT_REQUEST_TIMEOUT` will be used as the timeout. - */ - val timeout: Duration = DEFAULT_REQUEST_TIMEOUT, -) + /** Destructuring component for [timeout]. */ + public operator fun component5(): Duration = timeout + + /** Creates a copy of this [RequestOptions] with the specified fields replaced. */ + public fun copy( + relatedRequestId: RequestId? = this.relatedRequestId, + resumptionToken: String? = this.resumptionToken, + onResumptionToken: ((String) -> Unit)? = this.onResumptionToken, + onProgress: ProgressCallback? = this.onProgress, + timeout: Duration = this.timeout, + ): RequestOptions = RequestOptions(relatedRequestId, resumptionToken, onResumptionToken, onProgress, timeout) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + if (!super.equals(other)) return false + + other as RequestOptions + + return onProgress == other.onProgress && timeout == other.timeout + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + onProgress.hashCode() + result = 31 * result + timeout.hashCode() + return result + } + + override fun toString(): String = + "RequestOptions(relatedRequestId=$relatedRequestId, resumptionToken=$resumptionToken, onResumptionToken=$onResumptionToken, onProgress=$onProgress, timeout=$timeout)" +} /** * Extra data given to request handlers. + * + * One instance exists per in-flight inbound request. It is passed as the second parameter to + * low-level handlers registered via [Protocol.setRequestHandler] **and** installed into the + * handler coroutine's [CoroutineContext], so code nested anywhere under the handler can reach it + * via [currentRequestHandlerExtra] without parameter plumbing. + * + * @property requestId the JSON-RPC id of the request being handled + * @property method the request's method */ -public class RequestHandlerExtra +public class RequestHandlerExtra internal constructor( + public val requestId: RequestId, + public val method: Method, + private val protocol: Protocol, + internal val capturedTransport: Transport, +) : AbstractCoroutineContextElement(Key) { + + /** Context key for retrieving the extra from a handler coroutine's context. */ + public companion object Key : CoroutineContext.Key + + /** + * Sends a notification tagged with `relatedRequestId = `[requestId], letting transports + * associate it with the request being handled (e.g. route it onto the request's SSE stream). + */ + public suspend fun sendNotification(notification: Notification) { + protocol.notification(notification, relatedRequestId = requestId) + } + + /** + * Sends a request to the peer tagged with `relatedRequestId = `[requestId]. + * + * Mirrors [Protocol.request]; cancellation of the handler coroutine propagates into this call. + */ + public suspend fun sendRequest(request: Request, options: RequestOptions? = null): T { + val base = options ?: RequestOptions() + return protocol.request( + request, + RequestOptions( + relatedRequestId = requestId, + resumptionToken = base.resumptionToken, + onResumptionToken = base.onResumptionToken, + onProgress = base.onProgress, + timeout = base.timeout, + ), + ) + } +} + +/** The [RequestHandlerExtra] of the MCP request being handled by the current coroutine, or `null` outside a handler. */ +public suspend fun currentRequestHandlerExtra(): RequestHandlerExtra? = currentCoroutineContext()[RequestHandlerExtra] internal val COMPLETED = CompletableDeferred(Unit).also { it.complete(Unit) } +/** Cap on how many recently-cancelled outbound request ids are remembered to quiet late responses/progress. */ +private const val CANCELLED_REQUEST_IDS_REMEMBERED = 256 + +// Bypass set: exempt from both bound tiers so the connection cannot self-block. +// Responses are not listed — they are handled inline before dispatch. +private val CONTROL_METHODS: Set = setOf( + Method.Defined.Ping.value, + Method.Defined.NotificationsCancelled.value, + Method.Defined.NotificationsProgress.value, + Method.Defined.NotificationsInitialized.value, +) + /** * Implements MCP protocol framing on top of a pluggable transport, including * features like request/response linking, notifications, and progress. + * + * @property options protocol-level configuration; `null` falls back to defaults */ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptions?) { - public var transport: Transport? = null - private set + private val connectionRef: AtomicRef = atomic(null) + + /** The active transport, or `null` if not connected. */ + public val transport: Transport? + get() = connectionRef.value?.transport private val _requestHandlers: AtomicRef RequestResult?>> = atomic(persistentMapOf()) + + /** Registered request handlers keyed by method name. */ public val requestHandlers: Map< String, suspend ( @@ -134,20 +259,41 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio private val _notificationHandlers = atomic(persistentMapOf Unit>()) + + /** Registered notification handlers keyed by method name. */ public val notificationHandlers: Map Unit> get() = _notificationHandlers.value private val _responseHandlers: AtomicRef Unit>> = atomic(persistentMapOf()) + + /** Pending response handlers keyed by request ID. */ public val responseHandlers: Map Unit> get() = _responseHandlers.value - private val _progressHandlers: AtomicRef> = + private val _progressHandlers: AtomicRef> = atomic(persistentMapOf()) - public val progressHandlers: Map + + /** Registered progress callbacks keyed by progress token. */ + public val progressHandlers: Map get() = _progressHandlers.value + private val recentlyCancelledRequestIds: AtomicRef> = + atomic(persistentListOf()) + + /** Remembers a locally-cancelled outbound request id so its late response/progress is quieted. */ + private fun rememberCancelledRequestId(id: RequestId) { + recentlyCancelledRequestIds.update { current -> + val appended = current.adding(id) + if (appended.size > CANCELLED_REQUEST_IDS_REMEMBERED) appended.removingAt(0) else appended + } + } + + /** Whether [id] was recently cancelled locally (see [rememberCancelledRequestId]). */ + private fun isRecentlyCancelled(id: RequestId?): Boolean = + id != null && recentlyCancelledRequestIds.value.contains(id) + /** * Callback for when the connection is closed for any reason. * @@ -177,25 +323,79 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio public var fallbackNotificationHandler: (suspend (notification: JSONRPCNotification) -> Unit)? = null init { + setNotificationHandler(Method.Defined.NotificationsCancelled) { notification -> + handleCancelledNotification(notification) + COMPLETED + } + setNotificationHandler(Method.Defined.NotificationsProgress) { notification -> onProgress(notification) COMPLETED } setRequestHandler(Method.Defined.Ping) { _, _ -> - EmptyRequestResult() + EmptyResult() } } + /** + * Enables concurrent dispatch of inbound messages on the current connection. + * + * Called by subclasses when the MCP initialization phase completes (the client after the + * initialize exchange; the server session on receiving `notifications/initialized`). Until + * then, inbound messages are handled inline in arrival order. Reset by [connect]. + */ + protected fun enableConcurrentDispatch() { + val connection = connectionRef.value ?: return + connection.concurrentDispatchEnabled.value = true + } + + /** + * Router hook invoked whenever `notifications/initialized` is received, before handler lookup. + * + * Runs regardless of which notification handler is registered for the method, so replacing the + * handler via [setNotificationHandler] cannot disable subclass initialization logic. + */ + protected open fun onInitializedNotification() {} + /** * Attaches to the given transport, starts it, and starts listening for messages. * - * The Protocol object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward. + * The Protocol object assumes ownership of the Transport, replacing any callbacks that have + * already been set, and expects that it is the only user of the Transport instance going forward. + * + * @throws IllegalStateException if this Protocol is already connected; call [close] first. */ public open suspend fun connect(transport: Transport) { - this.transport = transport + val maxConcurrent = options?.maxConcurrentHandlers ?: DEFAULT_MAX_CONCURRENT_HANDLERS + val maxInFlight = options?.maxInFlightHandlers ?: DEFAULT_MAX_IN_FLIGHT_HANDLERS + require(maxConcurrent > 0) { "maxConcurrentHandlers must be positive, but was $maxConcurrent" } + require(maxInFlight >= maxConcurrent) { + "maxInFlightHandlers ($maxInFlight) must be >= maxConcurrentHandlers ($maxConcurrent)" + } + val handlerContext = (options?.handlerCoroutineContext ?: Dispatchers.Default).minusKey(Job) + val interceptor = handlerContext[ContinuationInterceptor] + if (interceptor === Dispatchers.Unconfined || + interceptor?.let { it::class.simpleName?.contains("Unconfined") } == true + ) { + logger.warn { + "handlerCoroutineContext uses an unconfined dispatcher ($interceptor); " + + "handler resumptions will run on the transport read loop and may block unrelated messages" + } + } + val connection = Connection( + transport = transport, + handlerScope = CoroutineScope(SupervisorJob() + handlerContext + CoroutineName("McpProtocol")), + executionSemaphore = Semaphore(maxConcurrent), + maxInFlightHandlers = maxInFlight, + ) + if (!connectionRef.compareAndSet(null, connection)) { + connection.handlerScope.cancel() + error("Protocol is already connected; close() the current connection before connecting a new transport") + } + transport.onClose { - doClose() + doClose(connection) } transport.onError { @@ -203,105 +403,277 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio } transport.onMessage { message -> + if (connectionRef.value !== connection) { + logger.trace { "Dropping message received after close: ${message::class.simpleName}" } + return@onMessage + } when (message) { is JSONRPCResponse -> onResponse(message, null) - is JSONRPCRequest -> onRequest(message) - is JSONRPCNotification -> onNotification(message) + is JSONRPCRequest -> dispatchRequest(connection, message) + is JSONRPCNotification -> dispatchNotification(connection, message) is JSONRPCError -> onResponse(null, message) + is JSONRPCEmptyMessage -> Unit } } - return transport.start() + logger.info { "Starting transport" } + try { + transport.start() + } catch (cause: Throwable) { + // Roll back so the Protocol can be reconnected after a failed start; idempotent with + // a transport-initiated doClose (both CAS on the same connection). + if (connectionRef.compareAndSet(connection, null)) { + connection.handlerScope.cancel() + } + throw cause + } } - private fun doClose() { - val handlersToNotify = _responseHandlers.value.values.toList() - _responseHandlers.getAndSet(persistentMapOf()) + private fun doClose(connection: Connection) { + // A stale onClose from a previous transport must not tear down the successor connection. + if (!connectionRef.compareAndSet(connection, null)) { + logger.trace { "Ignoring close signal from a stale transport" } + return + } + val handlersToNotify = _responseHandlers.getAndSet(persistentMapOf()).values.toList() _progressHandlers.getAndSet(persistentMapOf()) - transport = null + recentlyCancelledRequestIds.getAndSet(persistentListOf()) + connection.inFlightRequestJobs.getAndSet(persistentMapOf()) + connection.handlerScope.cancel() onClose() - val error = McpError(ErrorCode.Defined.ConnectionClosed.code, "Connection closed") + val error = McpException(RPCError.ErrorCode.CONNECTION_CLOSED, "Connection closed") for (handler in handlersToNotify) { handler(null, error) } } - private suspend fun onNotification(notification: JSONRPCNotification) { - LOGGER.trace { "Received notification: ${notification.method}" } - - val handler = notificationHandlers[notification.method] ?: fallbackNotificationHandler - - if (handler == null) { - LOGGER.trace { "No handler found for notification: ${notification.method}" } + private suspend fun dispatchRequest(connection: Connection, request: JSONRPCRequest) { + if (!connection.concurrentDispatchEnabled.value) { + // Serial phase (before the peer completes MCP initialization): inline, in arrival + // order — the delivering coroutine awaits the handler. + onRequest(request, connection, trackCancellation = false) return } + val bypass = request.method in CONTROL_METHODS + if (!bypass && !tryAdmit(connection)) { + rejectOverloadedRequest(connection, request) + return + } + // UNDISPATCHED starts the handler synchronously in the delivering coroutine, preserving + // arrival order, then moves to handlerCoroutineContext at its first suspension. + connection.handlerScope.launch(start = CoroutineStart.UNDISPATCHED) { + // Register before any suspension point so a following notifications/cancelled can find + // and cancel even a handler still parked on admission. + registerInFlight(connection, request.id, coroutineContext.job) + try { + if (bypass) { + onRequest(request, connection, trackCancellation = true) + } else { + connection.executionSemaphore.withPermit { + onRequest(request, connection, trackCancellation = true) + } + } + } finally { + if (!bypass) connection.inFlightCount.decrementAndGet() + } + } + } + + private suspend fun dispatchNotification(connection: Connection, notification: JSONRPCNotification) { + if (notification.method == Method.Defined.NotificationsInitialized.value) { + onInitializedNotification() + } + if (!connection.concurrentDispatchEnabled.value) { + onNotification(notification) + return + } + val bypass = notification.method in CONTROL_METHODS + if (!bypass && !tryAdmit(connection)) { + val dropped = IllegalStateException( + "Dropped notification ${notification.method}: too many in-flight messages", + ) + logger.warn { dropped.message } + onError(dropped) + return + } + connection.handlerScope.launch(start = CoroutineStart.UNDISPATCHED) { + try { + if (bypass) { + onNotification(notification) + } else { + connection.executionSemaphore.withPermit { onNotification(notification) } + } + } finally { + if (!bypass) connection.inFlightCount.decrementAndGet() + } + } + } + + private fun tryAdmit(connection: Connection): Boolean { + while (true) { + val current = connection.inFlightCount.value + if (current >= connection.maxInFlightHandlers) return false + if (connection.inFlightCount.compareAndSet(current, current + 1)) return true + } + } + + private suspend fun rejectOverloadedRequest(connection: Connection, request: JSONRPCRequest) { + logger.warn { + "Rejecting request ${request.method} (id: ${request.id}): " + + "in-flight handler limit (${connection.maxInFlightHandlers}) reached" + } try { - handler(notification) + connection.transport.send( + JSONRPCError( + id = request.id, + error = RPCError( + code = RPCError.ErrorCode.INTERNAL_ERROR, + message = "Server is busy: too many in-flight messages", + ), + ), + ) + } catch (e: CancellationException) { + throw e } catch (cause: Throwable) { - LOGGER.error(cause) { "Error handling notification: ${notification.method}" } + logger.error(cause) { "Failed to send busy rejection for request id: ${request.id}" } onError(cause) } } - private suspend fun onRequest(request: JSONRPCRequest) { - LOGGER.trace { "Received request: ${request.method} (id: ${request.id})" } + private fun handleCancelledNotification(notification: CancelledNotification) { + val connection = connectionRef.value ?: return + val requestId = notification.params.requestId + val job = connection.inFlightRequestJobs.value[requestId] + if (job == null) { + logger.trace { "Ignoring cancellation for unknown or already-completed request: $requestId" } + return + } + job.cancel("Cancelled by peer: ${notification.params.reason ?: "unknown"}") + } + + private suspend fun onNotification(notification: JSONRPCNotification) { + logger.trace { "Received notification: ${notification.method}" } + + val handler = notificationHandlers[notification.method] ?: fallbackNotificationHandler + + if (handler == null) { + logger.trace { "No handler found for notification: ${notification.method}" } + return + } + try { + handler(notification) + } catch (e: CancellationException) { + throw e + } catch (cause: Throwable) { + logger.error(cause) { "Error handling notification: ${notification.method}" } + onError(cause) + } + } + + private suspend fun onRequest(request: JSONRPCRequest, connection: Connection, trackCancellation: Boolean) { + logger.trace { "Received request: ${request.method} (id: ${request.id})" } val handler = requestHandlers[request.method] ?: fallbackRequestHandler + val capturedTransport = connection.transport if (handler === null) { - LOGGER.trace { "No handler found for request: ${request.method}" } + logger.trace { "No handler found for request: ${request.method}" } try { - transport?.send( - JSONRPCResponse( + capturedTransport.send( + JSONRPCError( id = request.id, - error = JSONRPCError( - ErrorCode.Defined.MethodNotFound, + error = RPCError( + code = RPCError.ErrorCode.METHOD_NOT_FOUND, message = "Server does not support ${request.method}", ), ), ) + } catch (e: CancellationException) { + throw e } catch (cause: Throwable) { - LOGGER.error(cause) { "Error sending method not found response" } + logger.error(cause) { "Error sending method not found response" } onError(cause) } return } - try { - val result = handler(request, RequestHandlerExtra()) - LOGGER.trace { "Request handled successfully: ${request.method} (id: ${request.id})" } + val extra = RequestHandlerExtra( + requestId = request.id, + method = Method.from(request.method), + protocol = this, + capturedTransport = capturedTransport, + ) - transport?.send( - JSONRPCResponse( - id = request.id, - result = result, - ), - ) - } catch (cause: Throwable) { - LOGGER.error(cause) { "Error handling request: ${request.method} (id: ${request.id})" } + // Registration in the in-flight registry already happened in dispatchRequest's launch + // block (synchronously, pre-suspension). Here we only need the job for suppression checks. + val handlerJob = if (trackCancellation) currentCoroutineContext().job else null + withContext(extra) { try { - transport?.send( + val result = handler(request, extra) + logger.trace { "Request handled successfully: ${request.method} (id: ${request.id})" } + if (handlerJob?.isCancelled == true) { + logger.trace { "Suppressing response for request cancelled by peer (id: ${request.id})" } + return@withContext + } + capturedTransport.send( JSONRPCResponse( id = request.id, - error = JSONRPCError( - code = ErrorCode.Defined.InternalError, - message = cause.message ?: "Internal error", - ), + result = result ?: EmptyResult(), ), ) - } catch (sendError: Throwable) { - LOGGER.error(sendError) { - "Failed to send error response for request: ${request.method} (id: ${request.id})" + } catch (e: CancellationException) { + // Intentional exception to the always-rethrow-CE convention, only at this boundary. + if (handlerJob == null) throw e // serial phase: inline dispatch is not response-suppressed + if (handlerJob.isCancelled) throw e // genuine peer or close cancel: suppress the response + // CE escaped a live handler job (e.g. a leaked inner withTimeout). Answer + // INTERNAL_ERROR so the peer does not hang until its own timeout. + respondWithError(capturedTransport, request, e) + } catch (cause: Throwable) { + if (handlerJob?.isCancelled == true) { + logger.trace { "Suppressing error response for request cancelled by peer (id: ${request.id})" } + return@withContext } - // Optionally implement fallback behavior here + respondWithError(capturedTransport, request, cause) } } } + private fun registerInFlight(connection: Connection, id: RequestId, job: Job) { + val previous = connection.inFlightRequestJobs.getAndUpdate { current -> current.putting(id, job) }[id] + if (previous != null) { + logger.warn { "Duplicate in-flight request id $id; replacing the previous handler job" } + } + job.invokeOnCompletion { + // Identity guard: never evict a successor job registered under a reused id. + connection.inFlightRequestJobs.update { current -> + if (current[id] === job) current.removing(id) else current + } + } + } + + private suspend fun respondWithError(transport: Transport, request: JSONRPCRequest, cause: Throwable) { + logger.error(cause) { "Error handling request: ${request.method} (id: ${request.id})" } + try { + val rpcError = if (cause is McpException) { + RPCError(code = cause.code, message = cause.message.orEmpty(), data = cause.data) + } else { + RPCError(code = RPCError.ErrorCode.INTERNAL_ERROR, message = cause.message ?: "Internal error") + } + transport.send(JSONRPCError(id = request.id, error = rpcError)) + } catch (e: CancellationException) { + throw e + } catch (sendError: Throwable) { + logger.error(sendError) { + "Failed to send error response for request: ${request.method} (id: ${request.id})" + } + onError(sendError) + } + } + private fun onProgress(notification: ProgressNotification) { - LOGGER.trace { + logger.trace { "Received progress notification: token=${notification.params.progressToken}, progress=${notification.params.progress}/${notification.params.total}" } val progress = notification.params.progress @@ -311,10 +683,14 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio val handler = _progressHandlers.value[progressToken] if (handler == null) { + if (isRecentlyCancelled(progressToken)) { + logger.trace { "Ignoring progress for a locally cancelled request: $progressToken" } + return + } val error = Error( "Received a progress notification for an unknown token: ${McpJson.encodeToString(notification)}", ) - LOGGER.error { error.message } + logger.error { error.message } onError(error) return } @@ -323,11 +699,11 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio } private fun onResponse(response: JSONRPCResponse?, error: JSONRPCError?) { - val messageId = response?.id + val messageId = response?.id ?: error?.id val oldResponseHandlers = _responseHandlers.getAndUpdate { current -> if (messageId != null && messageId in current) { - current.remove(messageId) + current.removing(messageId) } else { current } @@ -336,22 +712,30 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio val handler = oldResponseHandlers[messageId] if (handler != null) { - messageId?.let { msg -> _progressHandlers.update { it.remove(msg) } } + messageId?.let { msg -> _progressHandlers.update { it.removing(msg) } } } else { - onError(Error("Received a response for an unknown message ID: ${McpJson.encodeToString(response)}")) + if (isRecentlyCancelled(messageId)) { + logger.trace { "Ignoring response for a locally cancelled request: $messageId" } + return + } + onError( + IllegalStateException( + "Received a response for an unknown message ID: ${McpJson.encodeToString(error ?: response)}", + ), + ) return } if (response != null) { handler(response, null) } else { - check(error != null) - val error = McpError( - error.code.code, - error.message, - error.data, + checkNotNull(error) + val mcpException = McpException.fromError( + code = error.error.code, + message = error.error.message, + data = error.error.data, ) - handler(null, error) + handler(null, mcpException) } } @@ -389,149 +773,225 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio * Do not use this method to emit notifications! Use notification() instead. */ public suspend fun request(request: Request, options: RequestOptions? = null): T { - LOGGER.trace { "Sending request: ${request.method}" } + logger.trace { "Sending request: ${request.method}" } val result = CompletableDeferred() - val transport = this@Protocol.transport ?: throw Error("Not connected") + val connection = connectionRef.value ?: error("Not connected") + val transport = connection.transport if (this@Protocol.options?.enforceStrictCapabilities == true) { assertCapabilityForMethod(request.method) } - val message = request.toJSON() - val messageId = message.id - - if (options?.onProgress != null) { - LOGGER.trace { "Registering progress handler for request id: $messageId" } - _progressHandlers.update { current -> - current.put(messageId, options.onProgress) - } - } - - _responseHandlers.update { current -> - current.put(messageId) { response, error -> - if (error != null) { - result.completeExceptionally(error) - return@put + val jsonRpcRequest = request.toJSON().run { + options?.onProgress?.let { progressHandler -> + logger.trace { "Registering progress handler for request id: $id" } + _progressHandlers.update { current -> + current.putting(id, progressHandler) } - if (response?.error != null) { - result.completeExceptionally(IllegalStateException(response.error.toString())) - return@put + val paramsObject = (this.params as? JsonObject) ?: JsonObject(emptyMap()) + val metaObject = request.params?.meta?.json ?: JsonObject(emptyMap()) + + val updatedMeta = JsonObject( + metaObject + ("progressToken" to McpJson.encodeToJsonElement(id)), + ) + val updatedParams = JsonObject( + paramsObject + ("_meta" to updatedMeta), + ) + + this.copy(params = updatedParams) + } ?: this + } + val jsonRpcRequestId = jsonRpcRequest.id + + _responseHandlers.update { current -> + current.putting(jsonRpcRequestId) { response, error -> + if (error != null) { + result.completeExceptionally(error) + return@putting } try { @Suppress("UNCHECKED_CAST") result.complete(response!!.result as T) - } catch (error: Throwable) { - result.completeExceptionally(error) + } catch (e: Throwable) { + result.completeExceptionally(e) } } } - val cancel: suspend (Throwable) -> Unit = { reason: Throwable -> - _responseHandlers.update { current -> current.remove(messageId) } - _progressHandlers.update { current -> current.remove(messageId) } + if (connectionRef.value !== connection) { + // Connection closed or replaced during registration; nothing would complete this handler. + _responseHandlers.update { it.removing(jsonRpcRequestId) } + _progressHandlers.update { it.removing(jsonRpcRequestId) } + throw McpException(RPCError.ErrorCode.CONNECTION_CLOSED, "Connection closed") + } - val notification = CancelledNotification( - params = CancelledNotification.Params( - requestId = messageId, - reason = reason.message ?: "Unknown", - ), - ) + val cancelPending: suspend (reason: Throwable, notifyPeer: Boolean) -> Unit = { reason, notifyPeer -> + // Remember before removing: a racing onResponse/onProgress that sees the handler gone + // must also see the id, or it reports a late message as unknown. Order is load-bearing. + rememberCancelledRequestId(jsonRpcRequestId) + _responseHandlers.update { current -> current.removing(jsonRpcRequestId) } + _progressHandlers.update { current -> current.removing(jsonRpcRequestId) } - val serialized = JSONRPCNotification( - notification.method.value, - params = McpJson.encodeToJsonElement(notification), - ) - transport.send(serialized) + if (notifyPeer) { + val notification = CancelledNotification( + params = CancelledNotificationParams( + requestId = jsonRpcRequestId, + reason = reason.message ?: "Unknown", + ), + ) + transport.send(notification.toJSON(), options) + } result.completeExceptionally(reason) } + // The MCP spec forbids cancelling `initialize`; local cleanup still runs, + // but no notifications/cancelled goes on the wire (timeout and cancel paths alike). + val notifyPeerOnCancel = request.method != Method.Defined.Initialize + val timeout = options?.timeout ?: DEFAULT_REQUEST_TIMEOUT try { - withTimeout(timeout) { - LOGGER.trace { "Sending request message with id: $messageId" } - this@Protocol.transport?.send(message) + val response = withTimeoutOrNull(timeout) { + logger.trace { "Sending request message with id: $jsonRpcRequestId" } + transport.send(jsonRpcRequest, options) + result.await() + } + if (response == null) { + // Our own request timeout expired. An outer withTimeout's cancellation propagates + // through withTimeoutOrNull instead of returning null and is handled by the catch below. + logger.error { "Request timed out after ${timeout.inWholeMilliseconds}ms: ${request.method}" } + val timeoutError = McpException( + code = RPCError.ErrorCode.REQUEST_TIMEOUT, + message = "Request timed out", + data = JsonObject(mutableMapOf("timeout" to JsonPrimitive(timeout.inWholeMilliseconds))), + ) + withContext(NonCancellable) { + try { + cancelPending(timeoutError, notifyPeerOnCancel) + } catch (e: Throwable) { + logger.warn(e) { "Failed to notify peer about timed-out request" } + onError(e) + } + } + result.cancel() + throw timeoutError + } + return response + } catch (cause: CancellationException) { + // Plain caller cancellation, or an outer withTimeout that propagated through + // withTimeoutOrNull (only our own deadline returns null there). NonCancellable so the + // cancellation notification still goes out even though the caller's job is cancelled. + // A secondary send failure must never replace the original CancellationException. + withContext(NonCancellable) { + try { + cancelPending(cause, notifyPeerOnCancel) + } catch (e: Throwable) { + logger.warn(e) { "Failed to notify peer about cancelled request" } + onError(e) + } } - return result.await() - } catch (cause: TimeoutCancellationException) { - LOGGER.error { "Request timed out after ${timeout.inWholeMilliseconds}ms: ${request.method}" } - cancel( - McpError( - ErrorCode.Defined.RequestTimeout.code, - "Request timed out", - JsonObject(mutableMapOf("timeout" to JsonPrimitive(timeout.inWholeMilliseconds))), - ), - ) - result.cancel(cause) throw cause + } finally { + // Backstop for the failed-send path. The success, timeout and cancellation paths already + // removed these handlers, so this only bites when the outbound send failed. + _responseHandlers.update { it.removing(jsonRpcRequestId) } + _progressHandlers.update { it.removing(jsonRpcRequestId) } } } /** * Emits a notification, which is a one-way message that does not expect a response. */ - public suspend fun notification(notification: Notification) { - LOGGER.trace { "Sending notification: ${notification.method}" } + public suspend fun notification(notification: Notification, relatedRequestId: RequestId? = null) { + logger.trace { "Sending notification: ${notification.method}" } val transport = this.transport ?: error("Not connected") assertNotificationCapability(notification.method) + val sendOptions = relatedRequestId?.let { TransportSendOptions(relatedRequestId = it) } + val jsonRpcNotification = notification.toJSON() - val message = notification.toJSON() - transport.send(message) + transport.send(jsonRpcNotification, sendOptions) } /** * Registers a handler to invoke when this protocol object receives a request with the given method. * * Note that this will replace any previous request handler for the same method. + * + * Handlers may run concurrently after initialization and are cancelled cooperatively, so always + * re-throw [CancellationException]. Replacing a built-in control-method handler (`ping`, + * `notifications/cancelled`, `notifications/progress`, `notifications/initialized`) with slow + * code degrades connection liveness. */ public inline fun setRequestHandler( method: Method, noinline block: suspend (T, RequestHandlerExtra) -> RequestResult?, ) { - setRequestHandler(typeOf(), method, block) + setRequestHandlerInternal(method, block) } @PublishedApi - internal fun setRequestHandler( - requestType: KType, + @Suppress("UNCHECKED_CAST") + internal fun setRequestHandlerInternal( method: Method, block: suspend (T, RequestHandlerExtra) -> RequestResult?, ) { assertRequestHandlerCapability(method) - - val serializer = McpJson.serializersModule.serializer(requestType) + val wrapped = wrapRequestHandler(method, block) _requestHandlers.update { current -> - current.put(method.value) { request, extraHandler -> - val result = McpJson.decodeFromJsonElement(serializer, request.params) - val response = if (result != null) { - @Suppress("UNCHECKED_CAST") - block(result as T, extraHandler) - } else { - EmptyRequestResult() + current.putting(method.value) { jSONRPCRequest, extraHandler -> + val request = try { + jSONRPCRequest.fromJSON() + } catch (cause: SerializationException) { + throw McpException( + code = RPCError.ErrorCode.INVALID_PARAMS, + message = "Invalid params", + cause = cause, + ) } + val response = wrapped(request as T, extraHandler) response } } } + /** + * Subclass hook to wrap an incoming-request handler before it is registered. + * + * Called once by [setRequestHandler] during registration. Subclasses may return a + * new function that, when invoked, performs additional checks (capability gates, + * schema validation, etc.) before delegating to [block]. The default implementation + * is the identity. + */ + @Suppress("UNUSED_PARAMETER") + protected open fun wrapRequestHandler( + method: Method, + block: suspend (T, RequestHandlerExtra) -> RequestResult?, + ): suspend (T, RequestHandlerExtra) -> RequestResult? = block + /** * Removes the request handler for the given method. */ public fun removeRequestHandler(method: Method) { - _requestHandlers.update { current -> current.remove(method.value) } + _requestHandlers.update { current -> current.removing(method.value) } } /** * Registers a handler to invoke when this protocol object receives a notification with the given method. * * Note that this will replace any previous notification handler for the same method. + * + * Handlers may run concurrently after initialization and are cancelled cooperatively, so always + * re-throw [CancellationException]. Replacing a built-in control-method handler (`ping`, + * `notifications/cancelled`, `notifications/progress`, `notifications/initialized`) with slow + * code degrades connection liveness. Replacing `notifications/cancelled` in particular disables + * inbound request cancellation entirely, since that handler is what cancels the running job. */ public fun setNotificationHandler(method: Method, handler: (notification: T) -> Deferred) { _notificationHandlers.update { current -> - current.put(method.value) { + current.putting(method.value) { @Suppress("UNCHECKED_CAST") handler(it.fromJSON() as T) } @@ -542,6 +1002,22 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio * Removes the notification handler for the given method. */ public fun removeNotificationHandler(method: Method) { - _notificationHandlers.update { current -> current.remove(method.value) } + _notificationHandlers.update { current -> current.removing(method.value) } + } + + private class Connection( + val transport: Transport, + val handlerScope: CoroutineScope, + val executionSemaphore: Semaphore, + val maxInFlightHandlers: Int, + ) { + /** Init gate: inbound dispatch is inline/serial until this flips. */ + val concurrentDispatchEnabled = atomic(false) + + /** Admission tier: launched-but-not-completed handler jobs (running + parked). */ + val inFlightCount = atomic(0) + + /** In-flight request handler jobs, for `notifications/cancelled`. */ + val inFlightRequestJobs: AtomicRef> = atomic(persistentMapOf()) } } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt index 014e0ee..8102cf6 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt @@ -1,70 +1,134 @@ package io.modelcontextprotocol.kotlin.sdk.shared import io.github.oshai.kotlinlogging.KotlinLogging -import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.McpJson import kotlinx.io.Buffer +import kotlinx.io.IOException import kotlinx.io.indexOf import kotlinx.io.readString /** * Buffers a continuous stdio stream into discrete JSON-RPC messages. + * + * A single newline-terminated frame may not exceed an internal size cap (16 MiB): a peer that streams + * bytes without ever sending a newline triggers a [TooLongFrameException] from [readMessage] instead + * of growing the buffer without bound. A non-positive cap disables the check. */ -public class ReadBuffer { +public class ReadBuffer internal constructor(private val maxFrameSize: Int) { + + public constructor() : this(DEFAULT_MAX_FRAME_SIZE) + + internal companion object { + internal const val DEFAULT_MAX_FRAME_SIZE: Int = 16 * 1024 * 1024 + } private val logger = KotlinLogging.logger { } private val buffer: Buffer = Buffer() + /** Appends raw bytes to the internal buffer for subsequent message parsing. */ public fun append(chunk: ByteArray) { buffer.write(chunk) } + /** + * Reads and deserializes a JSON-RPC message from the input buffer. + * + * The method attempts to read lines from the buffer until a valid `JSONRPCMessage` is successfully + * deserialized. Blank lines are ignored, and if a deserialization error occurs, the method attempts + * to recover and process the message. + * + * Recovery involves attempting to parse from the first detected JSON object in the line when an + * error is encountered during deserialization. + * + * @return A deserialized `JSONRPCMessage` if successfully processed; otherwise, `null` if the + * input buffer is exhausted or no valid message is found. + */ public fun readMessage(): JSONRPCMessage? { - if (buffer.exhausted()) return null - var lfIndex = buffer.indexOf('\n'.code.toByte()) - val line = when (lfIndex) { - -1L -> return null + while (true) { + val line = readNextLine() ?: return null + if (line.isBlank()) continue - 0L -> { - buffer.skip(1) - return null + val message = try { + deserializeMessage(line) + } catch (e: Exception) { + logger.error(e) { "Failed to deserialize message from line: $line\nAttempting to recover..." } + tryRecover(line) } - - else -> { - var skipBytes = 1 - if (buffer[lfIndex - 1] == '\r'.code.toByte()) { - lfIndex -= 1 - skipBytes += 1 - } - val string = buffer.readString(lfIndex) - buffer.skip(skipBytes.toLong()) - string + if (message != null) { + return message } } - try { - return deserializeMessage(line) - } catch (e: Exception) { - logger.error(e) { "Failed to deserialize message from line: $line\nAttempting to recover..." } - // if there is a non-JSON object prefix, try to parse from the first '{' onward. - val braceIndex = line.indexOf('{') - if (braceIndex != -1) { - val trimmed = line.substring(braceIndex) - try { - return deserializeMessage(trimmed) - } catch (ignored: Exception) { - logger.error(ignored) { "Deserialization failed for line: $line\nSkipping..." } - } - } - } - - return null } + private fun readNextLine(): String? { + val lfIndex = if (buffer.exhausted()) -1L else buffer.indexOf('\n'.code.toByte()) + if (lfIndex == -1L) { + failIfFrameTooLong(buffer.size) + return null + } + failIfFrameTooLong(lfIndex) + + return if (lfIndex == 0L) { + buffer.skip(1) + "" + } else { + var skipBytes = 1 + var messageLength = lfIndex + if (buffer[lfIndex - 1] == '\r'.code.toByte()) { + messageLength -= 1 + skipBytes += 1 + } + val string = buffer.readString(messageLength) + buffer.skip(skipBytes.toLong()) + string + } + } + + private fun failIfFrameTooLong(frameSize: Long) { + if (maxFrameSize in 1..(line) +/** Serializes a [JSONRPCMessage] to its JSON string representation with a trailing newline. */ public fun serializeMessage(message: JSONRPCMessage): String = McpJson.encodeToString(message) + "\n" diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Transport.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Transport.kt index ba460f9..a89c102 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Transport.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Transport.kt @@ -1,7 +1,6 @@ package io.modelcontextprotocol.kotlin.sdk.shared -import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage -import kotlinx.coroutines.CompletableDeferred +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage /** * Describes the minimal contract for MCP transport that a client or server can communicate over. @@ -12,6 +11,8 @@ public interface Transport { * * This method should only be called after callbacks are installed, or else messages may be lost. * + * Method waits for the initialization flow to complete and then returns, so the transport is ready to send and receive user messages. + * * NOTE: This method should not be called explicitly when using Client, Server, or Protocol classes, * as they will implicitly call start(). */ @@ -19,11 +20,15 @@ public interface Transport { /** * Sends a JSON-RPC message (request or response). + * + * @param message the JSON-RPC message to send, either a request or a response + * @param options optional transport-specific options that control sending behavior; + * different transport implementations may support different options */ - public suspend fun send(message: JSONRPCMessage) + public suspend fun send(message: JSONRPCMessage, options: TransportSendOptions? = null) /** - * Closes the connection. + * Closes the transport. */ public suspend fun close() @@ -47,53 +52,3 @@ public interface Transport { */ public fun onMessage(block: suspend (JSONRPCMessage) -> Unit) } - -/** - * Implements [onClose], [onError] and [onMessage] functions of [Transport] providing - * corresponding [_onClose], [_onError] and [_onMessage] properties to use for an implementation. - */ -@Suppress("PropertyName") -public abstract class AbstractTransport : Transport { - protected var _onClose: (() -> Unit) = {} - private set - protected var _onError: ((Throwable) -> Unit) = {} - private set - - // to not skip messages - private val _onMessageInitialized = CompletableDeferred() - protected var _onMessage: (suspend ((JSONRPCMessage) -> Unit)) = { - _onMessageInitialized.await() - _onMessage.invoke(it) - } - private set - - override fun onClose(block: () -> Unit) { - val old = _onClose - _onClose = { - old() - block() - } - } - - override fun onError(block: (Throwable) -> Unit) { - val old = _onError - _onError = { e -> - old(e) - block(e) - } - } - - override fun onMessage(block: suspend (JSONRPCMessage) -> Unit) { - val old: suspend (JSONRPCMessage) -> Unit = when (_onMessageInitialized.isCompleted) { - true -> _onMessage - false -> { _ -> } - } - - _onMessage = { message -> - old(message) - block(message) - } - - _onMessageInitialized.complete(Unit) - } -} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions.kt new file mode 100644 index 0000000..5490288 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions.kt @@ -0,0 +1,56 @@ +package io.modelcontextprotocol.kotlin.sdk.shared + +import io.modelcontextprotocol.kotlin.sdk.types.RequestId + +/** + * Options for sending a JSON-RPC message through transport. + * + * @property relatedRequestId if present, + * `relatedRequestId` is used to indicate to the transport which incoming request to associate this outgoing message with. + * @property resumptionToken the resumption token used to continue long-running requests that were interrupted. + * This allows clients to reconnect and continue from where they left off, if supported by the transport. + * @property onResumptionToken a callback that is invoked when the resumption token changes, if supported by the transport. + * This allows clients to persist the latest token for potential reconnection. + */ +public open class TransportSendOptions( + public val relatedRequestId: RequestId? = null, + public val resumptionToken: String? = null, + public val onResumptionToken: ((String) -> Unit)? = null, +) { + /** Destructuring component for [relatedRequestId]. */ + public operator fun component1(): RequestId? = relatedRequestId + + /** Destructuring component for [resumptionToken]. */ + public operator fun component2(): String? = resumptionToken + + /** Destructuring component for [onResumptionToken]. */ + public operator fun component3(): ((String) -> Unit)? = onResumptionToken + + /** Creates a copy of this [TransportSendOptions] with the specified fields replaced. */ + public open fun copy( + relatedRequestId: RequestId? = this.relatedRequestId, + resumptionToken: String? = this.resumptionToken, + onResumptionToken: ((String) -> Unit)? = this.onResumptionToken, + ): TransportSendOptions = TransportSendOptions(relatedRequestId, resumptionToken, onResumptionToken) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as TransportSendOptions + + return relatedRequestId == other.relatedRequestId && + resumptionToken == other.resumptionToken && + onResumptionToken == other.onResumptionToken + } + + override fun hashCode(): Int { + var result = relatedRequestId?.hashCode() ?: 0 + result = 31 * result + (resumptionToken?.hashCode() ?: 0) + result = 31 * result + (onResumptionToken?.hashCode() ?: 0) + return result + } + + override fun toString(): String = + "TransportSendOptions(relatedRequestId=$relatedRequestId, resumptionToken=$resumptionToken, onResumptionToken=$onResumptionToken)" +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt index 4a93676..baf3723 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt @@ -1,10 +1,12 @@ package io.modelcontextprotocol.kotlin.sdk.shared +import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.websocket.Frame import io.ktor.websocket.WebSocketSession import io.ktor.websocket.close import io.ktor.websocket.readText -import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.McpJson import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.InternalCoroutinesApi @@ -14,9 +16,13 @@ import kotlinx.coroutines.job import kotlinx.coroutines.launch import kotlin.concurrent.atomics.AtomicBoolean import kotlin.concurrent.atomics.ExperimentalAtomicApi +import kotlin.coroutines.cancellation.CancellationException +/** WebSocket subprotocol identifier for MCP connections. */ public const val MCP_SUBPROTOCOL: String = "mcp" +private val logger = KotlinLogging.logger {} + /** * Abstract class representing a WebSocket transport for the Model Context Protocol (MCP). * Handles communication over a WebSocket session. @@ -40,6 +46,8 @@ public abstract class WebSocketMcpTransport : AbstractTransport() { protected abstract suspend fun initializeSession() override suspend fun start() { + logger.debug { "Starting websocket transport" } + if (!initialized.compareAndSet(expectedValue = false, newValue = true)) { error( "WebSocketClientTransport already started! " + @@ -54,6 +62,7 @@ public abstract class WebSocketMcpTransport : AbstractTransport() { val message = try { session.incoming.receive() } catch (_: ClosedReceiveChannelException) { + logger.debug { "Closed receive channel, exiting" } return@launch } @@ -66,6 +75,8 @@ public abstract class WebSocketMcpTransport : AbstractTransport() { try { val message = McpJson.decodeFromString(message.readText()) _onMessage.invoke(message) + } catch (e: CancellationException) { + throw e } catch (e: Exception) { _onError.invoke(e) throw e @@ -78,12 +89,13 @@ public abstract class WebSocketMcpTransport : AbstractTransport() { if (it != null) { _onError.invoke(it) } else { - _onClose.invoke() + invokeOnCloseCallback() } } } - override suspend fun send(message: JSONRPCMessage) { + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { + logger.debug { "Sending message" } if (!initialized.load()) { error("Not connected") } @@ -96,6 +108,7 @@ public abstract class WebSocketMcpTransport : AbstractTransport() { error("Not connected") } + logger.debug { "Closing websocket session" } session.close() session.coroutineContext.job.join() } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt deleted file mode 100644 index b687924..0000000 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt +++ /dev/null @@ -1,1745 +0,0 @@ -@file:Suppress("unused", "EnumEntryName") - -package io.modelcontextprotocol.kotlin.sdk - -import io.modelcontextprotocol.kotlin.sdk.shared.McpJson -import kotlinx.serialization.EncodeDefault -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.decodeFromJsonElement -import kotlinx.serialization.json.encodeToJsonElement -import kotlinx.serialization.json.jsonObject -import kotlin.concurrent.atomics.AtomicLong -import kotlin.concurrent.atomics.ExperimentalAtomicApi -import kotlin.concurrent.atomics.incrementAndFetch -import kotlin.jvm.JvmInline -import kotlin.time.ExperimentalTime -import kotlin.time.Instant - -public const val LATEST_PROTOCOL_VERSION: String = "2025-03-26" - -public val SUPPORTED_PROTOCOL_VERSIONS: Array = arrayOf( - LATEST_PROTOCOL_VERSION, - "2024-11-05", -) - -public const val JSONRPC_VERSION: String = "2.0" - -@OptIn(ExperimentalAtomicApi::class) -private val REQUEST_MESSAGE_ID: AtomicLong = AtomicLong(0L) - -/** - * A progress token, used to associate progress notifications with the original request. - * Stores message ID. - */ -public typealias ProgressToken = RequestId - -/** - * An opaque token used to represent a cursor for pagination. - */ -public typealias Cursor = String - -/** - * Represents an entity that includes additional metadata in its responses. - */ -@Serializable -public sealed interface WithMeta { - /** - * The protocol reserves this result property - * to allow clients and servers to attach additional metadata to their responses. - */ - @Suppress("PropertyName") - public val _meta: JsonObject - - public companion object { - public val Empty: CustomMeta = CustomMeta() - } -} - -/** - * An implementation of [WithMeta] containing custom metadata. - * - * @param _meta The JSON object holding metadata. Defaults to an empty JSON object. - */ -@Serializable -public class CustomMeta(override val _meta: JsonObject = EmptyJsonObject) : WithMeta - -/** - * Represents a method in the protocol, which can be predefined or custom. - */ -@Serializable(with = RequestMethodSerializer::class) -public sealed interface Method { - public val value: String - - /** - * Enum of predefined methods supported by the protocol. - */ - @Serializable - public enum class Defined(override val value: String) : Method { - Initialize("initialize"), - Ping("ping"), - ResourcesList("resources/list"), - ResourcesTemplatesList("resources/templates/list"), - ResourcesRead("resources/read"), - ResourcesSubscribe("resources/subscribe"), - ResourcesUnsubscribe("resources/unsubscribe"), - PromptsList("prompts/list"), - PromptsGet("prompts/get"), - NotificationsCancelled("notifications/cancelled"), - NotificationsInitialized("notifications/initialized"), - NotificationsProgress("notifications/progress"), - NotificationsMessage("notifications/message"), - NotificationsResourcesUpdated("notifications/resources/updated"), - NotificationsResourcesListChanged("notifications/resources/list_changed"), - NotificationsToolsListChanged("notifications/tools/list_changed"), - NotificationsRootsListChanged("notifications/roots/list_changed"), - NotificationsPromptsListChanged("notifications/prompts/list_changed"), - ToolsList("tools/list"), - ToolsCall("tools/call"), - LoggingSetLevel("logging/setLevel"), - SamplingCreateMessage("sampling/createMessage"), - CompletionComplete("completion/complete"), - RootsList("roots/list"), - ElicitationCreate("elicitation/create"), - } - - /** - * Represents a custom method defined by the user. - */ - @Serializable - public data class Custom(override val value: String) : Method -} - -/** - * Represents a request in the protocol. - */ -@Serializable(with = RequestPolymorphicSerializer::class) -public sealed interface Request { - public val method: Method -} - -/** - * Converts the request to a JSON-RPC request. - * - * @return The JSON-RPC request representation. - */ -public fun Request.toJSON(): JSONRPCRequest { - val fullJson = McpJson.encodeToJsonElement(this).jsonObject - val params = JsonObject(fullJson.filterKeys { it != "method" }) - return JSONRPCRequest( - method = method.value, - params = params, - jsonrpc = JSONRPC_VERSION, - ) -} - -/** - * Decodes a JSON-RPC request into a protocol-specific [Request]. - * - * @return The decoded [Request] or null - */ -internal fun JSONRPCRequest.fromJSON(): Request { - val requestData = JsonObject(params.jsonObject + ("method" to JsonPrimitive(method))) - val deserializer = selectRequestDeserializer(method) - return McpJson.decodeFromJsonElement(deserializer, requestData) -} - -/** - * A custom request with a specified method. - * - * @param method The method associated with the request. - */ -@Serializable -public open class CustomRequest(override val method: Method) : Request - -/** - * Represents a notification in the protocol. - */ -@Serializable(with = NotificationPolymorphicSerializer::class) -public sealed interface Notification { - public val method: Method - public val params: NotificationParams? -} - -/** - * Converts the notification to a JSON-RPC notification. - * - * @return The JSON-RPC notification representation. - */ -public fun Notification.toJSON(): JSONRPCNotification = JSONRPCNotification( - method = method.value, - params = McpJson.encodeToJsonElement(params), -) - -/** - * Decodes a JSON-RPC notification into a protocol-specific [Notification]. - * - * @return The decoded [Notification]. - */ -internal fun JSONRPCNotification.fromJSON(): Notification { - val data = buildJsonObject { - put("method", JsonPrimitive(method)) - put("params", params) - } - return McpJson.decodeFromJsonElement(data) -} - -/** - * Represents the result of a request, including additional metadata. - */ -@Serializable(with = RequestResultPolymorphicSerializer::class) -public sealed interface RequestResult : WithMeta - -/** - * An empty result for a request containing optional metadata. - * - * @param _meta Additional metadata for the response. Defaults to an empty JSON object. - */ -@Serializable -public data class EmptyRequestResult(override val _meta: JsonObject = EmptyJsonObject) : - ServerResult, - ClientResult - -/** - * A uniquely identifying ID for a request in JSON-RPC. - */ -@Serializable(with = RequestIdSerializer::class) -public sealed interface RequestId { - @Serializable - public data class StringId(val value: String) : RequestId - - @Serializable - public data class NumberId(val value: Long) : RequestId -} - -/** - * Represents a JSON-RPC message in the protocol. - */ -@Serializable(with = JSONRPCMessagePolymorphicSerializer::class) -public sealed interface JSONRPCMessage - -/** - * A request that expects a response. - */ -@OptIn(ExperimentalAtomicApi::class) -@Serializable -public data class JSONRPCRequest( - val id: RequestId = RequestId.NumberId(REQUEST_MESSAGE_ID.incrementAndFetch()), - val method: String, - val params: JsonElement = EmptyJsonObject, - val jsonrpc: String = JSONRPC_VERSION, -) : JSONRPCMessage - -/** - * A notification which does not expect a response. - */ -@Serializable -public data class JSONRPCNotification( - val method: String, - val params: JsonElement = EmptyJsonObject, - val jsonrpc: String = JSONRPC_VERSION, -) : JSONRPCMessage - -/** - * A successful (non-error) response to a request. - */ -@Serializable -public class JSONRPCResponse( - public val id: RequestId, - public val jsonrpc: String = JSONRPC_VERSION, - public val result: RequestResult? = null, - public val error: JSONRPCError? = null, -) : JSONRPCMessage { - - public fun copy( - id: RequestId = this.id, - jsonrpc: String = this.jsonrpc, - result: RequestResult? = this.result, - error: JSONRPCError? = this.error, - ): JSONRPCResponse = JSONRPCResponse(id, jsonrpc, result, error) -} - -/** - * An incomplete set of error codes that may appear in JSON-RPC responses. - */ -@Serializable(with = ErrorCodeSerializer::class) -public sealed interface ErrorCode { - public val code: Int - - @Serializable - public enum class Defined(override val code: Int) : ErrorCode { - // SDK error codes - ConnectionClosed(-1), - RequestTimeout(-2), - - // Standard JSON-RPC error codes - ParseError(-32700), - InvalidRequest(-32600), - MethodNotFound(-32601), - InvalidParams(-32602), - InternalError(-32603), - } - - @Serializable - public data class Unknown(override val code: Int) : ErrorCode -} - -/** - * A response to a request that indicates an error occurred. - */ -@Serializable -public data class JSONRPCError(val code: ErrorCode, val message: String, val data: JsonObject = EmptyJsonObject) : - JSONRPCMessage - -/** - * Base interface for notification parameters with optional metadata. - */ -@Serializable -public sealed interface NotificationParams : WithMeta - -/* Cancellation */ -/** - * This notification can be sent by either side to indicate that it is cancelling a previously issued request. - * - * The request SHOULD still be in-flight, but due to communication latency, it is always possible that this notification MAY arrive after the request has already finished. - * - * This notification indicates that the result will be unused, so any associated processing SHOULD cease. - * - * A client MUST NOT attempt to cancel its `initialize` request. - */ -@Serializable -public data class CancelledNotification(override val params: Params) : - ClientNotification, - ServerNotification { - override val method: Method = Method.Defined.NotificationsCancelled - - @Serializable - public data class Params( - /** - * The ID of the request to cancel. - * - * It MUST correspond to the ID of a request previously issued in the same direction. - */ - val requestId: RequestId, - /** - * An optional string describing the reason for the cancellation. This MAY be logged or presented to the user. - */ - val reason: String? = null, - override val _meta: JsonObject = EmptyJsonObject, - ) : NotificationParams -} - -/* Initialization */ -/** - * Describes the name and version of an MCP implementation. - */ -@Serializable -public data class Implementation(val name: String, val version: String) - -/** - * Capabilities a client may support. - * Known capabilities are defined here, in this, but this is not a closed set: - * any client can define its own, additional capabilities. - */ -@Serializable -public data class ClientCapabilities( - /** - * Experimental, non-standard capabilities that the client supports. - */ - val experimental: JsonObject? = EmptyJsonObject, - /** - * Present if the client supports sampling from an LLM. - */ - val sampling: JsonObject? = EmptyJsonObject, - /** - * Present if the client supports listing roots. - */ - val roots: Roots? = null, - /** - * Present if the client supports elicitation. - */ - val elicitation: JsonObject? = null, -) { - @Serializable - public data class Roots( - /** - * Whether the client supports issuing notifications for changes to the root list. - */ - val listChanged: Boolean?, - ) -} - -/** - * Represents a request sent by the client. - */ -// @Serializable(with = ClientRequestPolymorphicSerializer::class) -public interface ClientRequest : Request - -/** - * Represents a notification sent by the client. - */ -@Serializable(with = ClientNotificationPolymorphicSerializer::class) -public sealed interface ClientNotification : Notification - -/** - * Represents a result returned to the client. - */ -@Serializable(with = ClientResultPolymorphicSerializer::class) -public sealed interface ClientResult : RequestResult - -/** - * Represents a request sent by the server. - */ -// @Serializable(with = ServerRequestPolymorphicSerializer::class) -public sealed interface ServerRequest : Request - -/** - * Represents a notification sent by the server. - */ -@Serializable(with = ServerNotificationPolymorphicSerializer::class) -public sealed interface ServerNotification : Notification - -/** - * Represents a result returned by the server. - */ -@Serializable(with = ServerResultPolymorphicSerializer::class) -public sealed interface ServerResult : RequestResult - -/** - * Represents a request or notification for an unknown method. - * - * @param method The method that is unknown. - */ -@Serializable -public data class UnknownMethodRequestOrNotification( - override val method: Method, - override val params: NotificationParams? = null, -) : ClientNotification, - ClientRequest, - ServerNotification, - ServerRequest - -/** - * This request is sent from the client to the server when it first connects, asking it to begin initialization. - */ -@Serializable -public data class InitializeRequest( - val protocolVersion: String, - val capabilities: ClientCapabilities, - val clientInfo: Implementation, - override val _meta: JsonObject = EmptyJsonObject, -) : ClientRequest, - WithMeta { - override val method: Method = Method.Defined.Initialize -} - -/** - * Represents the capabilities that a server can support. - * - * @property experimental Experimental, non-standard capabilities that the server supports. - * @property sampling Present if the client supports sampling from an LLM. - * @property logging Present if the server supports sending log messages to the client. - * @property prompts Capabilities related to prompt templates offered by the server. - * @property resources Capabilities related to resources available on the server. - * @property tools Capabilities related to tools that can be called on the server. - */ -@Serializable -public data class ServerCapabilities( - val experimental: JsonObject? = EmptyJsonObject, - val sampling: JsonObject? = EmptyJsonObject, - val logging: JsonObject? = EmptyJsonObject, - val prompts: Prompts? = null, - val resources: Resources? = null, - val tools: Tools? = null, -) { - /** - * Capabilities related to prompt templates. - * - * @property listChanged Indicates if the server supports notifications when the prompt list changes. - */ - @Serializable - public data class Prompts( - /** - * Whether this server supports issuing notifications for changes to the prompt list. - */ - val listChanged: Boolean?, - ) - - /** - * Capabilities related to resources. - * - * @property subscribe Indicates if clients can subscribe to resource updates. - * @property listChanged Indicates if the server supports notifications when the resource list changes. - */ - @Serializable - public data class Resources( - /** - * Whether this server supports clients subscribing to resource updates. - */ - val subscribe: Boolean?, - /** - * Whether this server supports issuing notifications for changes to the resource list. - */ - val listChanged: Boolean?, - ) - - /** - * Capabilities related to tools. - * - * @property listChanged Indicates if the server supports notifications when the tool list changes. - */ - @Serializable - public data class Tools( - /** - * Whether this server supports issuing notifications for changes to the tool list. - */ - val listChanged: Boolean?, - ) -} - -/** - * After receiving an initialized request from the client, the server sends this response. - */ -@Serializable -public data class InitializeResult( - /** - * The version of the Model Context Protocol that the server wants to use. This may not match the version that the client requested. If the client cannot support this version, it MUST disconnect. - */ - val protocolVersion: String = LATEST_PROTOCOL_VERSION, - val capabilities: ServerCapabilities = ServerCapabilities(), - val serverInfo: Implementation, - override val _meta: JsonObject = EmptyJsonObject, -) : ServerResult - -/** - * This notification is sent from the client to the server after initialization has finished. - */ -@Serializable -public data class InitializedNotification(override val params: Params = Params()) : ClientNotification { - override val method: Method = Method.Defined.NotificationsInitialized - - @Serializable - public data class Params(override val _meta: JsonObject = EmptyJsonObject) : NotificationParams -} - -/* Ping */ -/** - * A ping, issued by either the server or the client, to check that the other party is still alive. - * The receiver must promptly respond, or else it may be disconnected. - */ -@Serializable -public class PingRequest : - ServerRequest, - ClientRequest { - override val method: Method = Method.Defined.Ping -} - -/** - * Represents the base interface for progress tracking. - */ -@Serializable -public sealed interface ProgressBase { - /** - * The progress thus far. This should increase every time progress is made, even if the total is unknown. - */ - public val progress: Double - - /** - * Total number of items to a process (or total progress required), if known. - */ - public val total: Double? - - /** - * An optional message describing the current progress. - */ - public val message: String? -} - -/* Progress notifications */ -/** - * Represents a progress notification. - * - * @property progress The current progress value. - * @property total The total progress required, if known. - */ -@Serializable -public open class Progress( - /** - * The progress thus far. This should increase every time progress is made, even if the total is unknown. - */ - override val progress: Double, - - /** - * Total number of items to a process (or total progress required), if known. - */ - override val total: Double?, - - /** - * An optional message describing the current progress. - */ - override val message: String?, -) : ProgressBase - -/** - * An out-of-band notification used to inform the receiver of a progress update for a long-running request. - */ -@Serializable -public data class ProgressNotification(override val params: Params) : - ClientNotification, - ServerNotification { - override val method: Method = Method.Defined.NotificationsProgress - - @Serializable - public data class Params( - /** - * The progress thus far. This should increase every time progress is made, even if the total is unknown. - */ - override val progress: Double, - /** - * The progress token, - * which was given in the initial request, - * used to associate this notification with the request that is proceeding. - */ - val progressToken: ProgressToken, - /** - * Total number of items to process (or total progress required), if known. - */ - override val total: Double? = null, - /** - * An optional message describing the current progress. - */ - override val message: String? = null, - override val _meta: JsonObject = EmptyJsonObject, - ) : NotificationParams, - ProgressBase -} - -/* Pagination */ -/** - * Represents a request supporting pagination. - */ -@Serializable -public sealed interface PaginatedRequest : - Request, - WithMeta { - /** - * The cursor indicating the pagination position. - */ - public val cursor: Cursor? - override val _meta: JsonObject -} - -/** - * Represents a paginated result of a request. - */ -@Serializable -public sealed interface PaginatedResult : RequestResult { - /** - * An opaque token representing the pagination position after the last returned result. - * If present, there may be more results available. - */ - public val nextCursor: Cursor? -} - -/* Resources */ -/** - * The contents of a specific resource or sub-resource. - */ -@Serializable(with = ResourceContentsPolymorphicSerializer::class) -public sealed interface ResourceContents { - /** - * The URI of this resource. - */ - public val uri: String - - /** - * The MIME type of this resource, if known. - */ - public val mimeType: String? -} - -/** - * Represents the text contents of a resource. - * - * @property text The text of the item. This must only be set if the item can actually be represented as text (not binary data). - */ -@Serializable -public data class TextResourceContents(val text: String, override val uri: String, override val mimeType: String?) : - ResourceContents - -/** - * Represents the binary contents of a resource encoded as a base64 string. - * - * @property blob A base64-encoded string representing the binary data of the item. - */ -@Serializable -public data class BlobResourceContents(val blob: String, override val uri: String, override val mimeType: String?) : - ResourceContents - -/** - * Represents resource contents with unknown or unspecified data. - */ -@Serializable -public data class UnknownResourceContents(override val uri: String, override val mimeType: String?) : ResourceContents - -/** - * A known resource that the server is capable of reading. - */ -@Serializable -public data class Resource( - /** - * The URI of this resource. - */ - val uri: String, - /** - * A human-readable name for this resource. - * - * Clients can use this to populate UI elements. - */ - val name: String, - /** - * A description of what this resource represents. - * - * Clients can use this to improve the LLM's understanding of available resources. - * It can be thought of as a "hint" to the model. - */ - val description: String?, - /** - * The MIME type of this resource, if known. - */ - val mimeType: String?, - /** - * The optional human-readable name of this resource for display purposes. - */ - val title: String? = null, - /** - * The optional size of this resource in bytes, if known. - */ - val size: Long? = null, - /** - * Optional annotations for the client. - */ - val annotations: Annotations? = null, -) - -/** - * A template description for resources available on the server. - */ -@Serializable -public data class ResourceTemplate( - /** - * A URI template (according to RFC 6570) that can be used to construct resource URIs. - */ - val uriTemplate: String, - /** - * A human-readable name for the type of resource this template refers to. - * - * Clients can use this to populate UI elements. - */ - val name: String, - /** - * A description of what this template is for. - * - * Clients can use this to improve the LLM's understanding of available resources. - * It can be thought of as a "hint" to the model. - */ - val description: String?, - /** - * The MIME type for all resources that match this template. This should only be included if all resources matching this template have the same type. - */ - val mimeType: String?, - /** - * The optional human-readable name of this resource for display purposes. - */ - val title: String? = null, - /** - * Optional annotations for the client. - */ - val annotations: Annotations? = null, -) - -/** - * Sent from the client to request a list of resources the server has. - */ -@Serializable -public data class ListResourcesRequest( - override val cursor: Cursor? = null, - override val _meta: JsonObject = EmptyJsonObject, -) : ClientRequest, - PaginatedRequest { - override val method: Method = Method.Defined.ResourcesList -} - -/** - * The server's response to a resources/list request from the client. - */ -@Serializable -public class ListResourcesResult( - public val resources: List, - override val nextCursor: Cursor? = null, - override val _meta: JsonObject = EmptyJsonObject, -) : ServerResult, - PaginatedResult - -/** - * Sent from the client to request a list of resource templates the server has. - */ -@Serializable -public data class ListResourceTemplatesRequest( - override val cursor: Cursor?, - override val _meta: JsonObject = EmptyJsonObject, -) : ClientRequest, - PaginatedRequest { - override val method: Method = Method.Defined.ResourcesTemplatesList -} - -/** - * The server's response to a resources/templates/list request from the client. - */ -@Serializable -public class ListResourceTemplatesResult( - public val resourceTemplates: List, - override val nextCursor: Cursor? = null, - override val _meta: JsonObject = EmptyJsonObject, -) : ServerResult, - PaginatedResult - -/** - * Sent from the client to the server to read a specific resource URI. - */ -@Serializable -public data class ReadResourceRequest(val uri: String, override val _meta: JsonObject = EmptyJsonObject) : - ClientRequest, - WithMeta { - override val method: Method = Method.Defined.ResourcesRead -} - -/** - * The server's response to a resources/read request from the client. - */ -@Serializable -public class ReadResourceResult( - public val contents: List, - override val _meta: JsonObject = EmptyJsonObject, -) : ServerResult - -/** - * An optional notification from the server to the client, - * informing it that the list of resources it can read from has changed. - * Servers may issue this without any previous subscription from the client. - */ -@Serializable -public data class ResourceListChangedNotification(override val params: Params = Params()) : ServerNotification { - override val method: Method = Method.Defined.NotificationsResourcesListChanged - - @Serializable - public data class Params(override val _meta: JsonObject = EmptyJsonObject) : NotificationParams -} - -/** - * Sent from the client to request resources/updated notifications from the server whenever a particular resource changes. - */ -@Serializable -public data class SubscribeRequest( - /** - * The URI of the resource to subscribe to. The URI can use any protocol; it is up to the server how to interpret it. - */ - val uri: String, - override val _meta: JsonObject = EmptyJsonObject, -) : ClientRequest, - WithMeta { - override val method: Method = Method.Defined.ResourcesSubscribe -} - -/** - * Sent from the client to request cancellation of resources/updated notifications from the server. This should follow a previous resources/subscribe request. - */ -@Serializable -public data class UnsubscribeRequest( - /** - * The URI of the resource to unsubscribe from. - */ - val uri: String, - override val _meta: JsonObject = EmptyJsonObject, -) : ClientRequest, - WithMeta { - override val method: Method = Method.Defined.ResourcesUnsubscribe -} - -/** - * A notification from the server to the client, informing it that a resource has changed and may need to be read again. This should only be sent if the client previously sent a resources/subscribe request. - */ -@Serializable -public data class ResourceUpdatedNotification(override val params: Params) : ServerNotification { - override val method: Method = Method.Defined.NotificationsResourcesUpdated - - @Serializable - public data class Params( - /** - * The URI of the resource that has been updated. This might be a sub-resource of the one that the client actually subscribed to. - */ - val uri: String, - override val _meta: JsonObject = EmptyJsonObject, - ) : NotificationParams -} - -/* Prompts */ -/** - * Describes an argument that a prompt can accept. - */ -@Serializable -public data class PromptArgument( - /** - * The name of the argument. - */ - val name: String, - /** - * A human-readable description of the argument. - */ - val description: String?, - /** - * Whether this argument must be provided. - */ - val required: Boolean?, -) - -/** - * A prompt or prompt template that the server offers. - */ -@Serializable -public class Prompt( - /** - * The name of the prompt or prompt template. - */ - public val name: String, - /** - * An optional description of what this prompt provides - */ - public val description: String?, - /** - * A list of arguments to use for templating the prompt. - */ - public val arguments: List?, -) - -/** - * Sent from the client to request a list of prompts and prompt templates the server has. - */ -@Serializable -public data class ListPromptsRequest( - override val cursor: Cursor? = null, - override val _meta: JsonObject = EmptyJsonObject, -) : ClientRequest, - PaginatedRequest { - override val method: Method = Method.Defined.PromptsList -} - -/** - * The server's response to a prompts/list request from the client. - */ -@Serializable -public class ListPromptsResult( - public val prompts: List, - override val nextCursor: Cursor? = null, - override val _meta: JsonObject = EmptyJsonObject, -) : ServerResult, - PaginatedResult - -/** - * Used by the client to get a prompt provided by the server. - */ -@Serializable -public data class GetPromptRequest( - /** - * The name of the prompt or prompt template. - */ - val name: String, - - /** - * Arguments to use for templating the prompt. - */ - val arguments: Map?, - - override val _meta: JsonObject = EmptyJsonObject, -) : ClientRequest, - WithMeta { - override val method: Method = Method.Defined.PromptsGet -} - -/** - * Represents the content of a prompt message. - */ -@Serializable(with = PromptMessageContentPolymorphicSerializer::class) -public sealed interface PromptMessageContent { - public val type: String -} - -/** - * Represents prompt message content that is either text, image or audio. - */ -@Serializable(with = PromptMessageContentMultimodalPolymorphicSerializer::class) -public sealed interface PromptMessageContentMultimodal : PromptMessageContent - -/** - * Text provided to or from an LLM. - */ -@Serializable -public data class TextContent( - /** - * The text content of the message. - */ - val text: String? = null, - - /** - * Optional annotations for the client. - */ - val annotations: Annotations? = null, -) : PromptMessageContentMultimodal { - override val type: String = TYPE - - public companion object { - public const val TYPE: String = "text" - } -} - -/** - * An image provided to or from an LLM. - */ -@Serializable -public data class ImageContent( - /** - * The base64-encoded image data. - */ - val data: String, - - /** - * The MIME type of the image. Different providers may support different image types. - */ - val mimeType: String, - - /** - * Optional annotations for the client. - */ - val annotations: Annotations? = null, -) : PromptMessageContentMultimodal { - override val type: String = TYPE - - public companion object { - public const val TYPE: String = "image" - } -} - -/** - * Audio provided to or from an LLM. - */ -@Serializable -public data class AudioContent( - /** - * The base64-encoded audio data. - */ - val data: String, - - /** - * The MIME type of the audio. Different providers may support different audio types. - */ - val mimeType: String, - - /** - * Optional annotations for the client. - */ - val annotations: Annotations? = null, -) : PromptMessageContentMultimodal { - override val type: String = TYPE - - public companion object { - public const val TYPE: String = "audio" - } -} - -/** - * Unknown content provided to or from an LLM. - */ -@Serializable -public data class UnknownContent(override val type: String) : PromptMessageContentMultimodal - -/** - * The contents of a resource, embedded into a prompt or tool call result. - */ -@Serializable -public data class EmbeddedResource( - /** - * The contents of the embedded resource. - */ - val resource: ResourceContents, - - /** - * Optional annotations for the client. - */ - val annotations: Annotations? = null, -) : PromptMessageContent { - override val type: String = TYPE - - public companion object { - public const val TYPE: String = "resource" - } -} - -/** - * Enum representing the role of a participant. - */ -@Suppress("EnumEntryName") -@Serializable -public enum class Role { - user, - assistant, -} - -/** - * Optional annotations for the client. - * The client can use annotations to inform how objects are used or displayed. - */ -@Serializable -public data class Annotations( - /** - * Describes who the intended customer of this object or data is. - */ - val audience: List?, - /** - * The moment the resource was last modified. - */ - @OptIn(ExperimentalTime::class) - val lastModified: Instant?, - /** - * Describes how important this data is for operating the server. - * - * A value of 1.0 means "most important", and indicates that the data is effectively required, - * while 0.0 means "less important", and indicates that the data is entirely optional. - */ - val priority: Double?, -) { - init { - require(priority == null || priority in 0.0..1.0) { "Priority must be between 0.0 and 1.0" } - } -} - -/** - * Describes a message returned as part of a prompt. - */ -@Serializable -public data class PromptMessage(val role: Role, val content: PromptMessageContent) - -/** - * The server's response to a prompts/get request from the client. - */ -@Serializable -public class GetPromptResult( - /** - * An optional description for the prompt. - */ - public val description: String?, - public val messages: List, - override val _meta: JsonObject = EmptyJsonObject, -) : ServerResult - -/** - * An optional notification from the server to the client, informing it that the list of prompts it offers has changed. - * Servers may issue this without any previous subscription from the client. - */ -@Serializable -public data class PromptListChangedNotification(override val params: Params = Params()) : ServerNotification { - override val method: Method = Method.Defined.NotificationsPromptsListChanged - - @Serializable - public data class Params(override val _meta: JsonObject = EmptyJsonObject) : NotificationParams -} - -/* Tools */ -/** - * Additional properties describing a Tool to clients. - * - * NOTE: all properties in ToolAnnotations are **hints**. - * They are not guaranteed to provide a faithful description of - * tool behavior (including descriptive properties like `title`). - * - * Clients should never make tool use decisions based on ToolAnnotations - * received from untrusted servers. - */ -@Serializable -public data class ToolAnnotations( - /** - * A human-readable title for the tool. - */ - val title: String?, - /** - * If true, the tool does not modify its environment. - * - * Default: false - */ - val readOnlyHint: Boolean? = false, - /** - * If true, the tool may perform destructive updates to its environment. - * If false, the tool performs only additive updates. - * - * (This property is meaningful only when `readOnlyHint == false`) - * - * Default: true - */ - val destructiveHint: Boolean? = true, - /** - * If true, calling the tool repeatedly with the same arguments - * will have no additional effect on the its environment. - * - * (This property is meaningful only when `readOnlyHint == false`) - * - * Default: false - */ - val idempotentHint: Boolean? = false, - /** - * If true, this tool may interact with an "open world" of external - * entities. If false, the tool's domain of interaction is closed. - * For example, the world of a web search tool is open, whereas that - * of a memory tool is not. - * - * Default: true - */ - val openWorldHint: Boolean? = true, -) - -/** - * Definition for a tool the client can call. - */ -@Serializable -public data class Tool( - /** - * The name of the tool. - */ - val name: String, - /** - * The title of the tool. - */ - val title: String?, - /** - * A human-readable description of the tool. - */ - val description: String?, - /** - * A JSON object defining the expected parameters for the tool. - */ - val inputSchema: Input, - /** - * An optional JSON object defining the expected output schema for the tool. - */ - val outputSchema: Output?, - /** - * Optional additional tool information. - */ - val annotations: ToolAnnotations?, -) { - @Serializable - public data class Input(val properties: JsonObject = EmptyJsonObject, val required: List? = null) { - @OptIn(ExperimentalSerializationApi::class) - @EncodeDefault - val type: String = "object" - } - - @Serializable - public data class Output(val properties: JsonObject = EmptyJsonObject, val required: List? = null) { - @OptIn(ExperimentalSerializationApi::class) - @EncodeDefault - val type: String = "object" - } -} - -/** - * Sent from the client to request a list of tools the server has. - */ -@Serializable -public data class ListToolsRequest( - override val cursor: Cursor? = null, - override val _meta: JsonObject = EmptyJsonObject, -) : ClientRequest, - PaginatedRequest { - override val method: Method = Method.Defined.ToolsList -} - -/** - * The server's response to a tools/list request from the client. - */ -@Serializable -public class ListToolsResult( - public val tools: List, - override val nextCursor: Cursor?, - override val _meta: JsonObject = EmptyJsonObject, -) : ServerResult, - PaginatedResult - -/** - * The server's response to a tool call. - */ -@Serializable -public sealed interface CallToolResultBase : ServerResult { - public val content: List - public val structuredContent: JsonObject? - public val isError: Boolean? get() = false -} - -/** - * The server's response to a tool call. - */ -@Serializable -public class CallToolResult( - override val content: List, - override val structuredContent: JsonObject? = null, - override val isError: Boolean? = false, - override val _meta: JsonObject = EmptyJsonObject, -) : CallToolResultBase - -/** - * [CallToolResult] extended with backwards compatibility to protocol version 2024-11-05. - */ -@Serializable -public class CompatibilityCallToolResult( - override val content: List, - override val structuredContent: JsonObject? = null, - override val isError: Boolean? = false, - override val _meta: JsonObject = EmptyJsonObject, - public val toolResult: JsonObject = EmptyJsonObject, -) : CallToolResultBase - -/** - * Used by the client to invoke a tool provided by the server. - */ -@Serializable -public data class CallToolRequest( - val name: String, - val arguments: JsonObject = EmptyJsonObject, - override val _meta: JsonObject = EmptyJsonObject, -) : ClientRequest, - WithMeta { - override val method: Method = Method.Defined.ToolsCall -} - -/** - * An optional notification from the server to the client, informing it that the list of tools it offers has changed. - * Servers may issue this without any previous subscription from the client. - */ -@Serializable -public data class ToolListChangedNotification(override val params: Params = Params()) : ServerNotification { - override val method: Method = Method.Defined.NotificationsToolsListChanged - - @Serializable - public data class Params(override val _meta: JsonObject = EmptyJsonObject) : NotificationParams -} - -/* Logging */ -/** - * The severity of a log message. - */ -@Suppress("EnumEntryName") -@Serializable -public enum class LoggingLevel { - debug, - info, - notice, - warning, - error, - critical, - alert, - emergency, -} - -/** - * Notification of a log message passed from server to client. - * If no logging level request has been sent from the client, - * the server MAY decide which messages to send automatically. - */ -@Serializable -public data class LoggingMessageNotification(override val params: Params) : ServerNotification { - override val method: Method = Method.Defined.NotificationsMessage - - @Serializable - public data class Params( - /** - * The severity of this log message. - */ - val level: LoggingLevel, - /** - * An optional name of the logger issuing this message. - */ - val logger: String? = null, - /** - * The data to be logged, such as a string message or an object. Any JSON serializable type is allowed here. - */ - val data: JsonElement, - override val _meta: JsonObject = EmptyJsonObject, - ) : NotificationParams - - /** - * A request from the client to the server to enable or adjust logging. - */ - @Serializable - public data class SetLevelRequest( - /** - * The level of logging that the client wants to receive from the server. The server should send all logs at this level and higher (i.e., more severe) to the client as notifications/logging/message. - */ - val level: LoggingLevel, - override val _meta: JsonObject = EmptyJsonObject, - ) : ClientRequest, - WithMeta { - override val method: Method = Method.Defined.LoggingSetLevel - } -} - -/* Sampling */ -/** - * Hints to use for model selection. - */ -@Serializable -public data class ModelHint( - /** - * A hint for a model name. - */ - val name: String?, -) - -/** - * The server's preferences for model selection, requested by the client during sampling. - */ -@Suppress("CanBeParameter") -@Serializable -public class ModelPreferences( - /** - * Optional hints to use for model selection. - */ - public val hints: List?, - /** - * How much to prioritize cost when selecting a model. - */ - public val costPriority: Double?, - /** - * How much to prioritize sampling speed (latency) when selecting a model. - */ - public val speedPriority: Double?, - /** - * How much to prioritize intelligence and capabilities when selecting a model. - */ - public val intelligencePriority: Double?, -) { - init { - require(costPriority == null || costPriority in 0.0..1.0) { - "costPriority must be in 0.0 <= x <= 1.0 value range" - } - - require(speedPriority == null || speedPriority in 0.0..1.0) { - "costPriority must be in 0.0 <= x <= 1.0 value range" - } - - require(intelligencePriority == null || intelligencePriority in 0.0..1.0) { - "intelligencePriority must be in 0.0 <= x <= 1.0 value range" - } - } -} - -/** - * Describes a message issued to or received from an LLM API. - */ -@Serializable -public data class SamplingMessage(val role: Role, val content: PromptMessageContentMultimodal) - -/** - * A request from the server to sample an LLM via the client. - * The client has full discretion over which model to select. - * The client should also inform the user before beginning sampling to allow them to inspect the request - * (human in the loop) and decide whether to approve it. - */ -@Serializable -public data class CreateMessageRequest( - val messages: List, - /** - * An optional system prompt the server wants to use it for sampling. The client MAY modify or omit this prompt. - */ - val systemPrompt: String?, - /** - * A request to include context from one or more MCP servers (including the caller), to be attached to the prompt. The client MAY ignore this request. - */ - val includeContext: IncludeContext?, - val temperature: Double?, - /** - * The maximum number of tokens to sample, as requested by the server. The client MAY choose to sample fewer tokens than requested. - */ - val maxTokens: Int, - val stopSequences: List?, - /** - * Optional metadata to pass through to the LLM provider. The format of this metadata is provider-specific. - */ - val metadata: JsonObject = EmptyJsonObject, - /** - * The server's preferences for which model to select. - */ - val modelPreferences: ModelPreferences?, - override val _meta: JsonObject = EmptyJsonObject, -) : ServerRequest, - WithMeta { - override val method: Method = Method.Defined.SamplingCreateMessage - - @Serializable - public enum class IncludeContext { none, thisServer, allServers } -} - -@Serializable(with = StopReasonSerializer::class) -public sealed interface StopReason { - public val value: String - - @Serializable - public data object EndTurn : StopReason { - override val value: String = "endTurn" - } - - @Serializable - public data object StopSequence : StopReason { - override val value: String = "stopSequence" - } - - @Serializable - public data object MaxTokens : StopReason { - override val value: String = "maxTokens" - } - - @Serializable - @JvmInline - public value class Other(override val value: String) : StopReason -} - -/** - * The client's response to a sampling/create_message request from the server. - * The client should inform the user before returning the sampled message to allow them to inspect the response - * (human in the loop) and decide whether to allow the server to see it. - */ -@Serializable -public data class CreateMessageResult( - /** - * The name of the model that generated the message. - */ - val model: String, - /** - * The reason why sampling stopped. - */ - val stopReason: StopReason? = null, - val role: Role, - val content: PromptMessageContentMultimodal, - override val _meta: JsonObject = EmptyJsonObject, -) : ClientResult - -/* Autocomplete */ -@Serializable(with = ReferencePolymorphicSerializer::class) -public sealed interface Reference { - public val type: String -} - -/** - * A reference to a resource or resource template definition. - */ -@Serializable -public data class ResourceTemplateReference( - /** - * The URI or URI template of the resource. - */ - val uri: String, -) : Reference { - override val type: String = TYPE - - public companion object { - public const val TYPE: String = "ref/resource" - } -} - -/** - * Identifies a prompt. - */ -@Serializable -public data class PromptReference( - /** - * The name of the prompt or prompt template - */ - val name: String, -) : Reference { - override val type: String = TYPE - - public companion object { - public const val TYPE: String = "ref/prompt" - } -} - -/** - * Identifies a prompt. - */ -@Serializable -public data class UnknownReference(override val type: String) : Reference - -/** - * A request from the client to the server to ask for completion options. - */ -@Serializable -public data class CompleteRequest( - val ref: Reference, - /** - * The argument's information - */ - val argument: Argument, - override val _meta: JsonObject = EmptyJsonObject, -) : ClientRequest, - WithMeta { - override val method: Method = Method.Defined.CompletionComplete - - @Serializable - public data class Argument( - /** - * The name of the argument - */ - val name: String, - /** - * The value of the argument to use for completion matching. - */ - val value: String, - ) -} - -/** - * The server's response to a completion/complete request - */ -@Serializable -public data class CompleteResult(val completion: Completion, override val _meta: JsonObject = EmptyJsonObject) : - ServerResult { - @Suppress("CanBeParameter") - @Serializable - public class Completion( - /** - * A list of completion values. Must not exceed 100 items. - */ - public val values: List, - /** - * The total number of completion options available. This can exceed the number of values actually sent in the response. - */ - public val total: Int?, - /** - * Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown. - */ - public val hasMore: Boolean?, - ) { - init { - require(values.size <= 100) { - "'values' field must not exceed 100 items" - } - } - } -} - -/* Roots */ -/** - * Represents a root directory or file that the server can operate on. - */ -@Serializable -public data class Root( - /** - * The URI identifying the root. This *must* start with file:// for now. - */ - val uri: String, - - /** - * An optional name for the root. - */ - val name: String?, -) { - init { - require(uri.startsWith("file://")) { - "'uri' param must start with 'file://'" - } - } -} - -/** - * Sent from the server to request a list of root URIs from the client. - */ -@Serializable -public class ListRootsRequest(override val _meta: JsonObject = EmptyJsonObject) : - ServerRequest, - WithMeta { - override val method: Method = Method.Defined.RootsList -} - -/** - * The client's response to a roots/list request from the server. - */ -@Serializable -public class ListRootsResult(public val roots: List, override val _meta: JsonObject = EmptyJsonObject) : - ClientResult - -/** - * A notification from the client to the server, informing it that the list of roots has changed. - */ -@Serializable -public data class RootsListChangedNotification(override val params: Params = Params()) : ClientNotification { - override val method: Method = Method.Defined.NotificationsRootsListChanged - - @Serializable - public data class Params(override val _meta: JsonObject = EmptyJsonObject) : NotificationParams -} - -/** - * Sent from the server to create an elicitation from the client. - */ -@Serializable -public data class CreateElicitationRequest( - public val message: String, - public val requestedSchema: RequestedSchema, - override val _meta: JsonObject = EmptyJsonObject, -) : ServerRequest, - WithMeta { - override val method: Method = Method.Defined.ElicitationCreate - - @Serializable - public data class RequestedSchema( - val properties: JsonObject = EmptyJsonObject, - val required: List? = null, - ) { - @OptIn(ExperimentalSerializationApi::class) - @EncodeDefault - val type: String = "object" - } -} - -/** - * The client's response to an elicitation/create request from the server. - */ -@Serializable -public data class CreateElicitationResult( - public val action: Action, - public val content: JsonObject? = null, - override val _meta: JsonObject = EmptyJsonObject, -) : ClientResult { - - init { - require(action == Action.accept || content == null) { - "Content can only be provided for an 'accept' action" - } - } - - @Serializable - public enum class Action { accept, decline, cancel } -} - -/** - * Represents an error specific to the MCP protocol. - * - * @property code The error code. - * @property message The error message. - * @property data Additional error data as a JSON object. - */ -public class McpError(public val code: Int, message: String, public val data: JsonObject = EmptyJsonObject) : - Exception() { - override val message: String = "MCP error $code: $message" -} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.util.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.util.kt deleted file mode 100644 index 07a0bf4..0000000 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.util.kt +++ /dev/null @@ -1,315 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk - -import io.github.oshai.kotlinlogging.KotlinLogging -import io.modelcontextprotocol.kotlin.sdk.LoggingMessageNotification.SetLevelRequest -import kotlinx.serialization.DeserializationStrategy -import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.buildClassSerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.json.JsonContentPolymorphicSerializer -import kotlinx.serialization.json.JsonDecoder -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonEncoder -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.json.long -import kotlinx.serialization.json.longOrNull - -private val logger = KotlinLogging.logger {} - -internal object ErrorCodeSerializer : KSerializer { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("io.modelcontextprotocol.kotlin.sdk.ErrorCode", PrimitiveKind.INT) - - override fun serialize(encoder: Encoder, value: ErrorCode) { - encoder.encodeInt(value.code) - } - - override fun deserialize(decoder: Decoder): ErrorCode { - val decodedInt = decoder.decodeInt() - return ErrorCode.Defined.entries.firstOrNull { it.code == decodedInt } - ?: ErrorCode.Unknown(decodedInt) - } -} - -internal object RequestMethodSerializer : KSerializer { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("io.modelcontextprotocol.kotlin.sdk.Method", PrimitiveKind.STRING) - - override fun serialize(encoder: Encoder, value: Method) { - encoder.encodeString(value.value) - } - - override fun deserialize(decoder: Decoder): Method { - val decodedString = decoder.decodeString() - return Method.Defined.entries.firstOrNull { it.value == decodedString } - ?: Method.Custom(decodedString) - } -} - -internal object StopReasonSerializer : KSerializer { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("io.modelcontextprotocol.kotlin.sdk.StopReason", PrimitiveKind.STRING) - - override fun serialize(encoder: Encoder, value: StopReason) { - encoder.encodeString(value.value) - } - - override fun deserialize(decoder: Decoder): StopReason { - val decodedString = decoder.decodeString() - return when (decodedString) { - StopReason.StopSequence.value -> StopReason.StopSequence - StopReason.MaxTokens.value -> StopReason.MaxTokens - StopReason.EndTurn.value -> StopReason.EndTurn - else -> StopReason.Other(decodedString) - } - } -} - -internal object ReferencePolymorphicSerializer : JsonContentPolymorphicSerializer(Reference::class) { - override fun selectDeserializer(element: JsonElement): DeserializationStrategy = - when (element.jsonObject.getValue("type").jsonPrimitive.content) { - ResourceTemplateReference.TYPE -> ResourceTemplateReference.serializer() - PromptReference.TYPE -> PromptReference.serializer() - else -> UnknownReference.serializer() - } -} - -internal object PromptMessageContentPolymorphicSerializer : - JsonContentPolymorphicSerializer(PromptMessageContent::class) { - override fun selectDeserializer(element: JsonElement): DeserializationStrategy = - when (element.jsonObject.getValue("type").jsonPrimitive.content) { - ImageContent.TYPE -> ImageContent.serializer() - TextContent.TYPE -> TextContent.serializer() - EmbeddedResource.TYPE -> EmbeddedResource.serializer() - AudioContent.TYPE -> AudioContent.serializer() - else -> UnknownContent.serializer() - } -} - -internal object PromptMessageContentMultimodalPolymorphicSerializer : - JsonContentPolymorphicSerializer(PromptMessageContentMultimodal::class) { - override fun selectDeserializer(element: JsonElement): DeserializationStrategy = - when (element.jsonObject.getValue("type").jsonPrimitive.content) { - ImageContent.TYPE -> ImageContent.serializer() - TextContent.TYPE -> TextContent.serializer() - AudioContent.TYPE -> AudioContent.serializer() - else -> UnknownContent.serializer() - } -} - -internal object ResourceContentsPolymorphicSerializer : - JsonContentPolymorphicSerializer(ResourceContents::class) { - override fun selectDeserializer(element: JsonElement): DeserializationStrategy { - val jsonObject = element.jsonObject - return when { - jsonObject.contains("text") -> TextResourceContents.serializer() - jsonObject.contains("blob") -> BlobResourceContents.serializer() - else -> UnknownResourceContents.serializer() - } - } -} - -internal fun selectRequestDeserializer(method: String): DeserializationStrategy { - selectClientRequestDeserializer(method)?.let { return it } - selectServerRequestDeserializer(method)?.let { return it } - return CustomRequest.serializer() -} - -internal fun selectClientRequestDeserializer(method: String): DeserializationStrategy? = when (method) { - Method.Defined.Ping.value -> PingRequest.serializer() - Method.Defined.Initialize.value -> InitializeRequest.serializer() - Method.Defined.CompletionComplete.value -> CompleteRequest.serializer() - Method.Defined.LoggingSetLevel.value -> SetLevelRequest.serializer() - Method.Defined.PromptsGet.value -> GetPromptRequest.serializer() - Method.Defined.PromptsList.value -> ListPromptsRequest.serializer() - Method.Defined.ResourcesList.value -> ListResourcesRequest.serializer() - Method.Defined.ResourcesTemplatesList.value -> ListResourceTemplatesRequest.serializer() - Method.Defined.ResourcesRead.value -> ReadResourceRequest.serializer() - Method.Defined.ResourcesSubscribe.value -> SubscribeRequest.serializer() - Method.Defined.ResourcesUnsubscribe.value -> UnsubscribeRequest.serializer() - Method.Defined.ToolsCall.value -> CallToolRequest.serializer() - Method.Defined.ToolsList.value -> ListToolsRequest.serializer() - else -> null -} - -private fun selectClientNotificationDeserializer(element: JsonElement): DeserializationStrategy? = - when (element.jsonObject.getValue("method").jsonPrimitive.content) { - Method.Defined.NotificationsCancelled.value -> CancelledNotification.serializer() - Method.Defined.NotificationsProgress.value -> ProgressNotification.serializer() - Method.Defined.NotificationsInitialized.value -> InitializedNotification.serializer() - Method.Defined.NotificationsRootsListChanged.value -> RootsListChangedNotification.serializer() - else -> null - } - -internal object ClientNotificationPolymorphicSerializer : - JsonContentPolymorphicSerializer(ClientNotification::class) { - override fun selectDeserializer(element: JsonElement): DeserializationStrategy = - selectClientNotificationDeserializer(element) - ?: UnknownMethodRequestOrNotification.serializer() -} - -internal fun selectServerRequestDeserializer(method: String): DeserializationStrategy? = when (method) { - Method.Defined.Ping.value -> PingRequest.serializer() - Method.Defined.SamplingCreateMessage.value -> CreateMessageRequest.serializer() - Method.Defined.RootsList.value -> ListRootsRequest.serializer() - else -> null -} - -internal fun selectServerNotificationDeserializer(element: JsonElement): DeserializationStrategy? = - when (element.jsonObject.getValue("method").jsonPrimitive.content) { - Method.Defined.NotificationsCancelled.value -> CancelledNotification.serializer() - Method.Defined.NotificationsProgress.value -> ProgressNotification.serializer() - Method.Defined.NotificationsMessage.value -> LoggingMessageNotification.serializer() - Method.Defined.NotificationsResourcesUpdated.value -> ResourceUpdatedNotification.serializer() - Method.Defined.NotificationsResourcesListChanged.value -> ResourceListChangedNotification.serializer() - Method.Defined.NotificationsToolsListChanged.value -> ToolListChangedNotification.serializer() - Method.Defined.NotificationsPromptsListChanged.value -> PromptListChangedNotification.serializer() - else -> null - } - -internal object ServerNotificationPolymorphicSerializer : - JsonContentPolymorphicSerializer(ServerNotification::class) { - override fun selectDeserializer(element: JsonElement): DeserializationStrategy = - selectServerNotificationDeserializer(element) - ?: UnknownMethodRequestOrNotification.serializer() -} - -internal object NotificationPolymorphicSerializer : - JsonContentPolymorphicSerializer(Notification::class) { - override fun selectDeserializer(element: JsonElement): DeserializationStrategy = - selectClientNotificationDeserializer(element) - ?: selectServerNotificationDeserializer(element) - ?: UnknownMethodRequestOrNotification.serializer() -} - -internal object RequestPolymorphicSerializer : - JsonContentPolymorphicSerializer(Request::class) { - override fun selectDeserializer(element: JsonElement): DeserializationStrategy { - val method = element.jsonObject.getOrElse("method") { null }?.jsonPrimitive?.content ?: run { - logger.error { "No method in $element" } - error("No method in $element") - } - - return selectClientRequestDeserializer(method) - ?: selectServerRequestDeserializer(method) - ?: UnknownMethodRequestOrNotification.serializer() - } -} - -private fun selectServerResultDeserializer(element: JsonElement): DeserializationStrategy? { - val jsonObject = element.jsonObject - return when { - jsonObject.contains("tools") -> ListToolsResult.serializer() - jsonObject.contains("resources") -> ListResourcesResult.serializer() - jsonObject.contains("resourceTemplates") -> ListResourceTemplatesResult.serializer() - jsonObject.contains("prompts") -> ListPromptsResult.serializer() - jsonObject.contains("capabilities") -> InitializeResult.serializer() - jsonObject.contains("description") -> GetPromptResult.serializer() - jsonObject.contains("completion") -> CompleteResult.serializer() - jsonObject.contains("toolResult") -> CompatibilityCallToolResult.serializer() - jsonObject.contains("contents") -> ReadResourceResult.serializer() - jsonObject.contains("content") -> CallToolResult.serializer() - else -> null - } -} - -private fun selectClientResultDeserializer(element: JsonElement): DeserializationStrategy? { - val jsonObject = element.jsonObject - return when { - jsonObject.contains("model") -> CreateMessageResult.serializer() - jsonObject.contains("roots") -> ListRootsResult.serializer() - jsonObject.contains("action") -> CreateElicitationResult.serializer() - else -> null - } -} - -internal object ServerResultPolymorphicSerializer : - JsonContentPolymorphicSerializer(ServerResult::class) { - override fun selectDeserializer(element: JsonElement): DeserializationStrategy = - selectServerResultDeserializer(element) - ?: EmptyRequestResult.serializer() -} - -internal object ClientResultPolymorphicSerializer : - JsonContentPolymorphicSerializer(ClientResult::class) { - override fun selectDeserializer(element: JsonElement): DeserializationStrategy = - selectClientResultDeserializer(element) - ?: EmptyRequestResult.serializer() -} - -internal object RequestResultPolymorphicSerializer : - JsonContentPolymorphicSerializer(RequestResult::class) { - override fun selectDeserializer(element: JsonElement): DeserializationStrategy = - selectClientResultDeserializer(element) - ?: selectServerResultDeserializer(element) - ?: EmptyRequestResult.serializer() -} - -internal object JSONRPCMessagePolymorphicSerializer : - JsonContentPolymorphicSerializer(JSONRPCMessage::class) { - override fun selectDeserializer(element: JsonElement): DeserializationStrategy { - val jsonObject = element.jsonObject - return when { - jsonObject.contains("message") -> JSONRPCError.serializer() - !jsonObject.contains("method") -> JSONRPCResponse.serializer() - jsonObject.contains("id") -> JSONRPCRequest.serializer() - else -> JSONRPCNotification.serializer() - } - } -} - -public val EmptyJsonObject: JsonObject = JsonObject(emptyMap()) - -public class RequestIdSerializer : KSerializer { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("RequestId") - - override fun deserialize(decoder: Decoder): RequestId { - val jsonDecoder = decoder as? JsonDecoder ?: error("Can only deserialize JSON") - val element = jsonDecoder.decodeJsonElement() - - return when (element) { - is JsonPrimitive -> when { - element.isString -> RequestId.StringId(element.content) - element.longOrNull != null -> RequestId.NumberId(element.long) - else -> error("Invalid RequestId type") - } - - else -> error("Invalid RequestId format") - } - } - - override fun serialize(encoder: Encoder, value: RequestId) { - val jsonEncoder = encoder as? JsonEncoder ?: error("Can only serialize JSON") - when (value) { - is RequestId.StringId -> jsonEncoder.encodeString(value.value) - is RequestId.NumberId -> jsonEncoder.encodeLong(value.value) - } - } -} - -/** - * Creates a [CallToolResult] with single [TextContent] and [meta]. - */ -public fun CallToolResult.Companion.ok(content: String, meta: JsonObject = EmptyJsonObject): CallToolResult = - CallToolResult( - content = listOf(TextContent(content)), - isError = false, - _meta = meta, - ) - -/** - * Creates a [CallToolResult] with single [TextContent] and [meta], with `isError` being true. - */ -public fun CallToolResult.Companion.error(content: String, meta: JsonObject = EmptyJsonObject): CallToolResult = - CallToolResult( - content = listOf(TextContent(content)), - isError = true, - _meta = meta, - ) diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/McpDsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/McpDsl.kt new file mode 100644 index 0000000..3c5e3ed --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/McpDsl.kt @@ -0,0 +1,12 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +/** + * DSL marker annotation for MCP builder classes. + * + * This annotation is used to prevent accidental access to outer DSL scopes + * within nested DSL blocks, ensuring type-safe and unambiguous builder usage. + * + * @see DslMarker + */ +@DslMarker +public annotation class McpDsl diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/McpException.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/McpException.kt new file mode 100644 index 0000000..6a9015b --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/McpException.kt @@ -0,0 +1,37 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import kotlinx.serialization.json.JsonElement +import kotlin.jvm.JvmOverloads + +/** + * Represents an error specific to the MCP protocol. + * + * @property code the MCP/JSON-RPC error code + * @param message the error message; used verbatim as [Exception.message] without an error-code prefix. + * Defaults to `"MCP error $code"` when not provided. + * @property data optional additional error payload as a JSON element + * @param cause the original cause + */ +public open class McpException @JvmOverloads public constructor( + public val code: Int, + message: String = "MCP error $code", + public val data: JsonElement? = null, + cause: Throwable? = null, +) : Exception(message, cause) { + internal companion object { + /** + * Reconstructs the most specific [McpException] subtype for a JSON-RPC error. + * + * Recognizes [RPCError.ErrorCode.URL_ELICITATION_REQUIRED] errors and returns a + * [UrlElicitationRequiredException] when [data] carries valid URL-mode elicitations; + * otherwise returns a plain [McpException]. Never throws — a malformed payload simply + * degrades to a plain [McpException]. + */ + fun fromError(code: Int, message: String, data: JsonElement?): McpException { + if (code == RPCError.ErrorCode.URL_ELICITATION_REQUIRED && data != null) { + UrlElicitationRequiredException.fromDataOrNull(message, data)?.let { return it } + } + return McpException(code = code, message = message, data = data) + } + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PingRequest.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PingRequest.kt new file mode 100644 index 0000000..8280404 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PingRequest.kt @@ -0,0 +1,23 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import kotlinx.serialization.EncodeDefault +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable + +/** + * Represents a ping request used to check if the connection is alive. + * + * @property params optional request parameters carrying metadata + */ +@Serializable +public data class PingRequest(override val params: BaseRequestParams? = null) : + ClientRequest, + ServerRequest { + @OptIn(ExperimentalSerializationApi::class) + @EncodeDefault + override val method: Method = Method.Defined.Ping + + /** Convenience accessor for [params]'s metadata. */ + public val meta: RequestMeta? + get() = params?.meta +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredException.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredException.kt new file mode 100644 index 0000000..f3da768 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredException.kt @@ -0,0 +1,65 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.encodeToJsonElement + +/** + * Payload carried by the [data][RPCError.data] of a [RPCError.ErrorCode.URL_ELICITATION_REQUIRED] error. + * + * @property elicitations The URL-mode elicitations that must be completed before the original request + * can succeed. Each entry is equivalent to an `elicitation/create` request. + */ +@Serializable +public data class UrlElicitationRequiredData(val elicitations: List) + +/** + * Signals that one or more URL-mode elicitations must be completed before a request can be processed. + * + * Serialized over the wire as a JSON-RPC error with code [RPCError.ErrorCode.URL_ELICITATION_REQUIRED] + * and [data][RPCError.data] holding the [elicitations]. A server throws this from a request handler to + * tell the client that a URL-mode elicitation is required; the client receives it from any request as a + * typed exception, equivalent to receiving an `elicitation/create` request. + * + * The client is responsible for obtaining explicit user consent and surfacing each [ElicitRequestURLParams] + * before navigation — the SDK never opens or validates URLs. + * + * Example client-side handling: + * ```kotlin + * try { + * client.callTool(name = "create_repo", arguments = args) + * } catch (e: UrlElicitationRequiredException) { + * for (elicitation in e.elicitations) { + * // Show elicitation.message and the target domain, then ask the user for consent. + * if (userConsents(elicitation.url)) openInBrowser(elicitation.url) + * // Await completion via Client.setElicitationCompleteHandler, then optionally retry the call. + * } + * } + * ``` + * + * @property elicitations The required URL-mode elicitations (always non-empty). + */ +public class UrlElicitationRequiredException( + public val elicitations: List, + message: String = defaultMessage(elicitations), +) : McpException( + code = RPCError.ErrorCode.URL_ELICITATION_REQUIRED, + message = message, + data = McpJson.encodeToJsonElement(UrlElicitationRequiredData(elicitations)), +) { + internal companion object { + private fun defaultMessage(elicitations: List): String = + if (elicitations.size > 1) "URL elicitations are required" else "URL elicitation is required" + + /** + * Reconstructs a [UrlElicitationRequiredException] from JSON-RPC error [data], or `null` when the + * payload is absent, malformed, or carries no elicitations. Never throws. + */ + fun fromDataOrNull(message: String, data: JsonElement): UrlElicitationRequiredException? = + runCatching { McpJson.decodeFromJsonElement(data).elicitations } + .getOrNull() + ?.takeIf { it.isNotEmpty() } + ?.let { UrlElicitationRequiredException(it, message) } + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.dsl.kt new file mode 100644 index 0000000..0d8b2b7 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.dsl.kt @@ -0,0 +1,178 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonObjectBuilder +import kotlinx.serialization.json.buildJsonObject + +/** + * DSL builder for constructing [ClientCapabilities] instances. + * + * This builder is used within [InitializeRequestBuilder] to configure client capabilities. + * All capabilities are optional - the presence of a capability indicates support for that feature. + * + * ## Available Functions (all optional) + * - [sampling] - Indicates support for sampling from an LLM + * - [roots] - Indicates support for listing roots + * - [elicitation] - Indicates support for elicitation from the server + * - [experimental] - Defines experimental, non-standard capabilities + * + * Example usage within [buildInitializeRequest][buildInitializeRequest]: + * ```kotlin + * val request = buildInitializeRequest { + * protocolVersion = "1.0" + * capabilities { + * sampling(ClientCapabilities.Sampling()) + * roots(listChanged = true) + * experimental { + * put("customFeature", JsonPrimitive(true)) + * } + * } + * info("MyClient", "1.0.0") + * } + * ``` + * + * @see ClientCapabilities + * @see InitializeRequestBuilder.capabilities + */ +@McpDsl +public class ClientCapabilitiesBuilder @PublishedApi internal constructor() { + private var sampling: ClientCapabilities.Sampling? = null + private var roots: ClientCapabilities.Roots? = null + private var elicitation: ClientCapabilities.Elicitation? = null + private var extensions: Map? = null + private var experimental: JsonObject? = null + + /** + * Sampling capability configuration. See [ClientCapabilities.Sampling]. + * + * Pass `ClientCapabilities.Sampling()` to enable base sampling with no sub-capabilities. + * Construct `ClientCapabilities.Sampling(tools = EmptyJsonObject, context = EmptyJsonObject)` + * directly to enable tools/context sub-capabilities. + * + * Example: + * ```kotlin + * capabilities { + * sampling(ClientCapabilities.Sampling(tools = EmptyJsonObject)) + * } + * ``` + * + * @param value The sampling capability configuration + */ + public fun sampling(value: ClientCapabilities.Sampling) { + this.sampling = value + } + + /** + * Indicates that the client supports listing roots. + * + * Example with listChanged notification: + * ```kotlin + * capabilities { + * roots(listChanged = true) + * } + * ``` + * + * Example without listChanged: + * ```kotlin + * capabilities { + * roots() + * } + * ``` + * + * @param listChanged Whether the client will emit notifications when the list of roots changes + */ + public fun roots(listChanged: Boolean? = null) { + this.roots = ClientCapabilities.Roots(listChanged) + } + + /** + * Indicates that the client supports elicitation from the server. + * + * Use [ClientCapabilities.elicitation] for default empty configuration. + * + * Example: + * ```kotlin + * capabilities { + * elicitation(ClientCapabilities.elicitation) + * } + * ``` + * + * @param value The elicitation capability configuration + */ + public fun elicitation(value: ClientCapabilities.Elicitation) { + this.elicitation = value + } + + /** + * Defines extensions that the client supports. + * + * Extension identifiers use the format `{vendor-prefix}/{extension-name}`, + * e.g., `"io.modelcontextprotocol/ui"`. Each value is an extension-specific + * settings object; an empty [JsonObject] indicates no settings. + * + * Example: + * ```kotlin + * capabilities { + * extensions(mapOf( + * "io.modelcontextprotocol/ui" to buildJsonObject { + * put("mimeTypes", JsonArray(listOf(JsonPrimitive("text/html")))) + * } + * )) + * } + * ``` + * + * @param value The map of extension identifiers to their settings + */ + public fun extensions(value: Map) { + this.extensions = value + } + + /** + * Defines experimental, non-standard capabilities that the client supports. + * + * Example: + * ```kotlin + * capabilities { + * experimental(buildJsonObject { + * put("customFeature", JsonPrimitive(true)) + * put("version", JsonPrimitive("1.0")) + * }) + * } + * ``` + * + * @param value The experimental capabilities configuration + */ + public fun experimental(value: JsonObject) { + this.experimental = value + } + + /** + * Defines experimental, non-standard capabilities that the client supports using a DSL builder. + * + * Example: + * ```kotlin + * capabilities { + * experimental { + * put("customFeature", JsonPrimitive(true)) + * put("beta", JsonObject(mapOf( + * "enabled" to JsonPrimitive(true), + * "version" to JsonPrimitive("2.0") + * ))) + * } + * } + * ``` + * + * @param block Lambda for building the experimental capabilities configuration + */ + public fun experimental(block: JsonObjectBuilder.() -> Unit): Unit = experimental(buildJsonObject(block)) + + /** Constructs the [ClientCapabilities] instance from the current builder state. */ + @PublishedApi + internal fun build(): ClientCapabilities = ClientCapabilities( + sampling = sampling, + roots = roots, + elicitation = elicitation, + extensions = extensions, + experimental = experimental, + ) +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.kt new file mode 100644 index 0000000..ecb8c48 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.kt @@ -0,0 +1,311 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject + +/** + * Describes an MCP (Model Context Protocol) implementation. + * + * This metadata helps clients identify and display information about servers or clients, + * including their name, version, and optional branding elements like icons and website links. + * + * @property name The programmatic identifier for this implementation. + * Intended for logical use and API identification. If [title] is not provided, + * this should be used as a fallback display name. + * @property version The version string of this implementation (e.g., "1.0.0", "2.1.3-beta"). + * @property title Optional human-readable display name for this implementation. + * Intended for UI and end-user contexts, optimized to be easily understood + * even by those unfamiliar with domain-specific terminology. + * If not provided, [name] should be used for display purposes. + * @property websiteUrl Optional URL of the website for this implementation. + * Can be used to provide documentation, support, or additional information. + * @property icons Optional set of sized icons that clients can display in their user interface. + * See [Icon] for supported formats and requirements. + */ +@Serializable +public data class Implementation( + val name: String, + val version: String, + val title: String? = null, + val websiteUrl: String? = null, + val icons: List? = null, +) + +/** + * Capabilities that a client may support. + * + * Known capabilities are defined here, but this is not a closed set: any client + * can define its own additional capabilities through the [experimental] field. + * + * The presence of a capability object (non-null value) indicates that the client + * supports that capability. + * + * @property sampling Present if the client supports sampling from an LLM. + * Use [ClientCapabilities.Sampling] to configure tools/context sub-capabilities. + * @property roots Present if the client supports listing roots. + * @property elicitation Present if the client supports elicitation from the server. + * Use [ClientCapabilities.Elicitation] to configure form/url mode sub-capabilities. + * @property tasks Present if the client supports task-augmented requests, listing, or cancellation. + * @property experimental Experimental, non-standard capabilities that the client supports. + * Keys are capability names, values are capability-specific configuration objects. + * @property extensions Optional extensions that the client supports. + * Keys are extension identifiers (e.g., `"io.modelcontextprotocol/ui"`), + * values are extension-specific settings objects. + */ +@Serializable +public data class ClientCapabilities( + public val sampling: Sampling? = null, + public val roots: Roots? = null, + public val elicitation: Elicitation? = null, + public val tasks: Tasks? = null, + public val experimental: JsonObject? = null, + public val extensions: Map? = null, +) { + + /** + * Source-compatibility constructor retaining the older `sampling: JsonObject?` / + * `elicitation: JsonObject?` shapes. Any non-null [sampling] is converted to an + * empty [Sampling], and any non-null [elicitation] is converted to an empty + * [Elicitation] (sub-capabilities cannot be recovered from the old opaque + * `JsonObject`). + */ + @Deprecated( + "ClientCapabilities.sampling and ClientCapabilities.elicitation are now typed. " + + "Pass typed ClientCapabilities.Sampling? / ClientCapabilities.Elicitation? " + + "instead of JsonObject?.", + ReplaceWith( + "ClientCapabilities(" + + "sampling?.let { ClientCapabilities.Sampling() }, " + + "roots, " + + "elicitation?.let { ClientCapabilities.Elicitation() }, " + + "tasks = null, " + + "experimental = experimental, " + + "extensions = extensions)", + ), + level = DeprecationLevel.WARNING, + ) + public constructor( + sampling: JsonObject?, + roots: Roots? = null, + elicitation: JsonObject? = null, + experimental: JsonObject? = null, + extensions: Map? = null, + ) : this( + sampling = sampling?.let { Sampling() }, + roots = roots, + elicitation = elicitation?.let { Elicitation() }, + tasks = null, + experimental = experimental, + extensions = extensions, + ) + + /** + * sub-capabilities for sampling. + * + * @property context Present if the client supports context inclusion via + * [CreateMessageRequestParams.includeContext] with values other than [IncludeContext.None]. + * Servers SHOULD avoid non-`none` values when this field is absent. + * @property tools Present if the client supports tool use via + * [CreateMessageRequestParams.tools] / [CreateMessageRequestParams.toolChoice]. + * Servers MUST NOT send `tools`/`toolChoice` when this field is absent. + */ + @Serializable + public data class Sampling(public val context: JsonObject? = null, public val tools: JsonObject? = null) + + /** + * @property sampling convenience value to enable the base sampling capability with no sub-capabilities + * @property elicitation convenience value to enable the elicitation capability with default form mode + */ + public companion object { + public val sampling: Sampling = Sampling() + public val elicitation: Elicitation = Elicitation() + } + + /** + * Indicates that the client supports listing roots. + * + * Roots are the top-level directories or locations that the server can access. + * When present (non-null), the server can query available roots and optionally + * receive notifications when the roots list changes. + * + * @property listChanged Whether the client supports notifications for changes to the roots list. + * If true, the client will send notifications when roots are added or removed. + */ + @Serializable + public data class Roots(val listChanged: Boolean? = null) + + /** + * Sub-capabilities for elicitation. + * + * An empty [Elicitation] (both fields null) is equivalent to declaring `form` mode only. + * + * @property form Present if the client supports form-mode elicitation + * (in-band structured data collection). + * @property url Present if the client supports url-mode elicitation + * (out-of-band interaction via URL navigation). + */ + @Serializable + public data class Elicitation(public val form: JsonObject? = null, public val url: JsonObject? = null) + + /** + * Sub-capabilities for tasks (client side). + * + * Declares which client-side requests can be augmented with task execution, + * and whether the client supports listing and cancelling tasks. + * + * @property list Present if the client supports the `tasks/list` operation. + * @property cancel Present if the client supports the `tasks/cancel` operation. + * @property requests Present if the client supports task-augmented sampling and/or elicitation requests. + */ + @Serializable + public data class Tasks( + public val list: JsonObject? = null, + public val cancel: JsonObject? = null, + public val requests: Requests? = null, + ) { + /** + * Task-augmentable client-side request categories. + * + * @property sampling Present if the client supports task-augmented `sampling/createMessage` requests. + * @property elicitation Present if the client supports task-augmented `elicitation/create` requests. + */ + @Serializable + public data class Requests( + public val sampling: Sampling? = null, + public val elicitation: Elicitation? = null, + ) { + /** + * @property createMessage Present if the client supports task-augmented `sampling/createMessage` requests. + */ + @Serializable + public data class Sampling(public val createMessage: JsonObject? = null) + + /** + * @property create Present if the client supports task-augmented `elicitation/create` requests. + */ + @Serializable + public data class Elicitation(public val create: JsonObject? = null) + } + } +} + +/** + * Whether the client supports url-mode elicitation (out-of-band interaction via URL navigation). + * + * `false` when the client declared no elicitation capability at all, or only form mode (an empty + * [ClientCapabilities.Elicitation] is treated as form mode only). + */ +public val ClientCapabilities.Elicitation?.supportsUrl: Boolean + get() = this?.url != null + +/** + * Capabilities that a server may support. + * + * Known capabilities are defined here, but this is not a closed set: any server + * can define its own additional capabilities through the [experimental] field. + * + * The presence of a capability object (non-null value) indicates that the server + * supports that capability. + * + * @property tools Present if the server offers any tools to call. + * @property resources Present if the server offers any resources to read. + * @property prompts Present if the server offers any prompt templates. + * @property logging Present if the server supports sending log messages to the client. + * @property completions Present if the server supports argument autocompletion suggestions. + * Keys are capability names, values are capability-specific configuration objects. + * @property tasks Present if the server supports task-augmented requests, listing, or cancellation. + * @property experimental Experimental, non-standard capabilities that the server supports. + * @property extensions Optional extensions that the server supports. + * Keys are extension identifiers (e.g., `"io.modelcontextprotocol/ui"`), + * values are extension-specific settings objects. + */ +@Serializable +public data class ServerCapabilities( + val tools: Tools? = null, + val resources: Resources? = null, + val prompts: Prompts? = null, + val logging: JsonObject? = null, + val completions: JsonObject? = null, + val tasks: Tasks? = null, + val experimental: JsonObject? = null, + val extensions: Map? = null, +) { + + /** + * @property Logging convenience value to enable the logging capability + * @property Completions convenience value to enable the completions capability + */ + public companion object { + public val Logging: JsonObject = EmptyJsonObject + public val Completions: JsonObject = EmptyJsonObject + } + + /** + * Indicates that the server offers tools to call. + * + * When present (non-null), clients can list and invoke tools (functions, actions, etc.) + * provided by the server. + * + * @property listChanged Whether this server supports notifications for changes to the tool list. + * If true, the server will send notifications when tools are added, modified, or removed. + */ + @Serializable + public data class Tools(val listChanged: Boolean? = null) + + /** + * Indicates that the server offers resources to read. + * + * When present (non-null), clients can list and read resources (files, data sources, etc.) + * provided by the server. + * + * @property listChanged Whether this server supports notifications for changes to the resource list. + * If true, the server will send notifications when resources are added or removed. + * @property subscribe Whether this server supports subscribing to resource updates. + * If true, clients can subscribe to receive notifications when specific + * resources are modified. + */ + @Serializable + public data class Resources(val listChanged: Boolean? = null, val subscribe: Boolean? = null) + + /** + * Indicates that the server offers prompt templates. + * + * When present (non-null), clients can list and invoke prompts provided by the server. + * + * @property listChanged Whether this server supports notifications for changes to the prompt list. + * If true, the server will send notifications when prompts are added, modified, or removed. + */ + @Serializable + public data class Prompts(val listChanged: Boolean? = null) + + /** + * Sub-capabilities for tasks (server side). + * + * Declares which server-side requests can be augmented with task execution, + * and whether the server supports listing and cancelling tasks. + * + * @property list Present if the server supports the `tasks/list` operation. + * @property cancel Present if the server supports the `tasks/cancel` operation. + * @property requests Present if the server supports task-augmented tool call requests. + */ + @Serializable + public data class Tasks( + public val list: JsonObject? = null, + public val cancel: JsonObject? = null, + public val requests: Requests? = null, + ) { + /** + * Task-augmentable server-side request categories. + * + * @property tools Present if the server supports task-augmented `tools/call` requests. + */ + @Serializable + public data class Requests(public val tools: Tools? = null) { + /** + * @property call Present if the server supports task-augmented `tools/call` requests. + */ + @Serializable + public data class Tools(public val call: JsonObject? = null) + } + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt new file mode 100644 index 0000000..f65b1ed --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt @@ -0,0 +1,177 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject + +// ============================================================================ +// Protocol Version Constants +// ============================================================================ + +/** The latest supported MCP protocol version string. */ +public const val LATEST_PROTOCOL_VERSION: String = "2025-11-25" + +/** The default protocol version used when negotiation is not performed. */ +public const val DEFAULT_NEGOTIATED_PROTOCOL_VERSION: String = "2025-03-26" + +/** All MCP protocol versions supported by this SDK. */ +public val SUPPORTED_PROTOCOL_VERSIONS: List = listOf( + LATEST_PROTOCOL_VERSION, + "2025-06-18", + "2025-03-26", + "2024-11-05", +) + +// ============================================================================ +// Base Interfaces +// ============================================================================ + +/** + * Represents an entity that includes additional metadata in its responses. + */ +@Serializable +public sealed interface WithMeta { + /** Optional metadata attached to this entity. */ + @SerialName("_meta") + public val meta: JsonObject? +} + +// ============================================================================ +// Tokens +// ============================================================================ + +/** + * A progress token, used to associate progress notifications with the original request. + */ +public typealias ProgressToken = RequestId + +/** Creates a [ProgressToken] from a string value. */ +public fun ProgressToken(value: String): ProgressToken = RequestId(value) + +/** Creates a [ProgressToken] from a numeric value. */ +public fun ProgressToken(value: Long): ProgressToken = RequestId(value) + +// ============================================================================ +// Visual Elements +// ============================================================================ + +/** + * An optionally sized icon that can be displayed in a user interface. + * + * Icons help clients provide visual branding and identification for MCP implementations. + * + * **Security considerations:** + * - Consumers SHOULD ensure URLs serving icons are from the same domain as the client/server + * or a trusted domain to prevent malicious content. + * - Consumers SHOULD take appropriate precautions when rendering SVGs as they can contain + * executable JavaScript. Consider sanitizing SVG content or rendering in isolated contexts. + * + * @property src A standard URI pointing to an icon resource. + * Maybe an HTTP/HTTPS URL or a data: URI with Base64-encoded image data. + * Example: "https://example.com/icon.png" or "data:image/png;base64,iVBORw0KG..." + * @property mimeType Optional MIME type override if the source MIME type is missing or generic. + * For example, "image/png", "image/jpeg", or "image/svg+xml". + * Useful when the URL doesn't include a file extension or uses a generic MIME type. + * @property sizes Optional array of strings that specify sizes at which the icon can be used. + * Each string should be in WxH format (e.g., "48x48", "96x96") or "any" for + * scalable formats like SVG. If not provided, the client should assume that + * the icon can be used at any size. + * @property theme Optional specifier for the theme this icon is designed for. + * [Theme.Light] indicates the icon is designed for a light background, + * [Theme.Dark] indicates the icon is designed for a dark background. + * If not provided, the client should assume the icon can be used with any theme. + */ +@Serializable +public data class Icon( + val src: String, + val mimeType: String? = null, + val sizes: List? = null, + val theme: Theme? = null, +) { + /** + * The theme context for which an icon is designed. + */ + @Serializable + public enum class Theme { + /** Icon designed for use with a light background */ + @SerialName("light") + Light, + + /** Icon designed for use with a dark background */ + @SerialName("dark") + Dark, + } +} + +// ============================================================================ +// Roles and References +// ============================================================================ + +/** + * The sender or recipient of messages and data in a conversation. + */ +@Serializable +public enum class Role { + @SerialName("user") + User, + + @SerialName("assistant") + Assistant, +} + +/** + * Base interface for reference types in the protocol. + * + * References are used to point to other entities (prompts, resources, etc.) + * without including their full definitions. + */ +@Serializable(with = ReferencePolymorphicSerializer::class) +public sealed interface Reference { + /** Discriminator identifying the reference subtype. */ + public val type: ReferenceType +} + +/** + * Discriminator for [Reference] subtypes used in completion and other operations. + * + * @property value serialized string representation of this reference type + */ +@Serializable +public enum class ReferenceType(public val value: String) { + @SerialName("ref/prompt") + Prompt("ref/prompt"), + + @SerialName("ref/resource") + ResourceTemplate("ref/resource"), +} + +// ============================================================================ +// Annotations and Metadata +// ============================================================================ + +/** + * Optional annotations for the client. + * + * The client can use annotations to inform how objects are used or displayed. + * + * @property audience Describes who the intended customer of this object or data is. + * Can include multiple entries to indicate content useful for multiple audiences + * (e.g., [Role.user, Role.assistant]). + * @property priority Describes how important this data is for operating the server. + * A value of 1.0 means "most important" and indicates that the data is effectively required, + * while 0.0 means "least important" and indicates that the data is entirely optional. + * Should be a value between 0.0 and 1.0. + * @property lastModified The moment the resource was last modified, as an ISO 8601 formatted string + * (e.g., "2025-01-12T15:00:58Z"). + * Examples: last activity timestamp in an open file, timestamp when the resource was attached, etc. + */ +@Serializable +public data class Annotations( + val audience: List? = null, + val priority: Double? = null, + val lastModified: String? = null, +) { + init { + require(priority == null || priority in 0.0..1.0) { "Priority must be between 0.0 and 1.0" } + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.dsl.kt new file mode 100644 index 0000000..29d3c99 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.dsl.kt @@ -0,0 +1,170 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * Creates a [CompleteRequest] using a type-safe DSL builder. + * + * ## Required + * - [argument][CompleteRequestBuilder.argument] - Sets the argument name and value to complete + * - [ref][CompleteRequestBuilder.ref] - Sets the reference to a prompt or resource template + * + * ## Optional + * - [context][CompleteRequestBuilder.context] - Adds additional context for the completion + * - [meta][CompleteRequestBuilder.meta] - Adds metadata to the request + * + * Example with [PromptReference]: + * ```kotlin + * val request = buildCompleteRequest { + * argument("query", "user input") + * ref(PromptReference("searchPrompt")) + * } + * ``` + * + * Example with [ResourceTemplateReference]: + * ```kotlin + * val request = buildCompleteRequest { + * argument("path", "/users/123") + * ref(ResourceTemplateReference("file:///{path}")) + * context { + * put("userId", "123") + * put("role", "admin") + * } + * } + * ``` + * + * @param block Configuration lambda for setting up the completion request + * @return A configured [CompleteRequest] instance + * @see CompleteRequestBuilder + */ +@OptIn(ExperimentalContracts::class) +@ExperimentalMcpApi +public inline fun buildCompleteRequest(block: CompleteRequestBuilder.() -> Unit): CompleteRequest { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + return CompleteRequestBuilder().apply(block).build() +} + +/** + * DSL builder for constructing [CompleteRequest] instances. + * + * This builder provides methods to configure completion requests for prompts or resource templates. + * Both [argument] and [ref] are required; [context] is optional. + * + * @see buildCompleteRequest + */ +@McpDsl +public class CompleteRequestBuilder @PublishedApi internal constructor() : RequestBuilder() { + private var arg: CompleteRequestParams.Argument? = null + private var ref: Reference? = null + private var ctx: CompleteRequestParams.Context? = null + + /** + * Sets the argument for completion. + * + * This method specifies the name and value of the argument to be completed. + * This is a required field and must be called exactly once. + * + * Example: + * ```kotlin + * completeRequest { + * argument("query", "SELECT * FROM use") + * // ... other configuration + * } + * ``` + * + * @param name The name of the argument to complete + * @param value The partial or full value of the argument + */ + public fun argument(name: String, value: String) { + arg = CompleteRequestParams.Argument(name, value) + } + + /** + * Sets the reference to a prompt or resource template. + * + * This method specifies which prompt or resource template the completion request refers to. + * + * Example with prompt: + * ```kotlin + * completeRequest { + * ref(PromptReference("sqlQuery", "SQL Query Builder")) + * // ... other configuration + * } + * ``` + * + * Example with resource template: + * ```kotlin + * completeRequest { + * ref(ResourceTemplateReference("file:///{path}")) + * // ... other configuration + * } + * ``` + * + * @param value The [Reference] (either [PromptReference] or [ResourceTemplateReference]) + */ + public fun ref(value: Reference) { + ref = value + } + + /** + * Sets additional context for the completion request using a Map. + * + * This method allows providing additional key-value pairs that may be relevant + * for generating completions. This is an optional field. + * + * Example: + * ```kotlin + * completeRequest { + * context(mapOf("userId" to "123", "role" to "admin")) + * // ... other configuration + * } + * ``` + * + * @param arguments A map of context key-value pairs + */ + public fun context(arguments: Map) { + ctx = CompleteRequestParams.Context(arguments) + } + + /** + * Sets additional context for the completion request using a DSL builder. + * + * This method allows providing additional key-value pairs that may be relevant + * for generating completions using a type-safe builder syntax. This is an optional field. + * + * Example: + * ```kotlin + * completeRequest { + * context { + * put("userId", "123") + * put("role", "admin") + * put("environment", "production") + * } + * // ... other configuration + * } + * ``` + * + * @param block Lambda with receiver for building the context map + */ + public fun context(block: MutableMap.() -> Unit) { + ctx = CompleteRequestParams.Context(buildMap(block)) + } + + @PublishedApi + override fun build(): CompleteRequest { + val argument = requireNotNull(arg) { + "Missing required field 'argument(name, value)'. Example: argument(\"query\", \"user input\")" + } + + val reference = requireNotNull(ref) { + "Missing required field 'ref(Reference)'. Use ref(PromptReference(\"name\")) " + + "or ref(ResourceTemplateReference(\"uri\"))" + } + + val params = CompleteRequestParams(argument = argument, ref = reference, context = ctx, meta = meta) + return CompleteRequest(params) + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.kt new file mode 100644 index 0000000..1eb5329 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.kt @@ -0,0 +1,119 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.modelcontextprotocol.kotlin.sdk.types.CompleteRequestParams.Argument +import io.modelcontextprotocol.kotlin.sdk.types.CompleteRequestParams.Context +import kotlinx.serialization.EncodeDefault +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject + +/** + * A request from the client to the server to ask for completion options. + * + * @property params The request parameters containing the argument to complete and its context. + */ +@Serializable +public data class CompleteRequest(override val params: CompleteRequestParams) : ClientRequest { + @OptIn(ExperimentalSerializationApi::class) + @EncodeDefault + public override val method: Method = Method.Defined.CompletionComplete + + /** + * The argument's information for which completion options are requested. + */ + public val argument: Argument + get() = params.argument + + /** + * A reference to either a prompt or resource template to complete within. + */ + public val ref: Reference + get() = params.ref + + /** + * Additional, context for generating completions. + */ + public val context: Context? + get() = params.context + + /** Convenience accessor for [params]'s metadata. */ + public val meta: RequestMeta? + get() = params.meta +} + +/** + * Parameters for the completion request. + * + * @property argument The argument's information for which completion options are requested. + * @property ref A reference to either a prompt or resource template to complete within. + * @property context Additional, optional context for generating completions. + * @property meta Optional metadata for this request. May include a progressToken for + * out-of-band progress notifications. + */ +@Serializable +public data class CompleteRequestParams( + val argument: Argument, + val ref: Reference, + val context: Context? = null, + @SerialName("_meta") + override val meta: RequestMeta? = null, +) : RequestParams { + /** + * The argument for which completion is being requested. + * + * @property name The name of the argument being completed. + * @property value The partial value of the argument to use for completion matching. + * This is typically the text the user has typed so far. + */ + @Serializable + public data class Argument(val name: String, val value: String) + + /** + * Additional context to help generate more relevant completions. + * + * @property arguments Previously resolved variables in a URI template or prompt. + * These can be used to provide context-aware completions. + * For example, if completing a file path, this might contain the repository or directory context. + */ + @Serializable + public data class Context(val arguments: Map? = null) +} + +/** + * The server's response to a [CompleteRequest]. + * + * Provides completion options for prompt or resource template arguments, + * along with pagination information if there are many possible completions. + * + * @property completion The completion options and metadata about available results. + * @property meta Optional metadata for this response. See MCP specification for details on _meta usage. + */ +@Serializable +public data class CompleteResult( + public val completion: Completion, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : ServerResult { + + /** + * Completion options and pagination information. + * + * @property values An array of completion values. Must not exceed 100 items. + * Each value represents a possible completion for the requested argument. + * @property total The total number of completion options available. + * This can exceed the number of values actually sent in the response, + * indicating that pagination or filtering may be needed. + * @property hasMore Indicates whether there are additional completion options beyond + * those provided in the current response, even if the exact total is unknown. + * Use this when the complete set of completions is too large to calculate upfront. + */ + @Serializable + public data class Completion(val values: List, val total: Int? = null, val hasMore: Boolean? = null) { + init { + require(values.size <= 100) { + "Completion 'values' must not exceed 100 items, got ${values.size}" + } + } + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.dsl.kt new file mode 100644 index 0000000..8718dec --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.dsl.kt @@ -0,0 +1,273 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonObjectBuilder +import kotlinx.serialization.json.buildJsonObject + +/** + * Base DSL builder for constructing [MediaContent] instances. + * + * This abstract class provides common functionality for all media content builders: + * - [annotations] - Optional metadata annotations + * - [meta] - Optional additional metadata + * + * Concrete implementations: + * - [TextContentBuilder] - For text content + * - [ImageContentBuilder] - For image content (base64-encoded) + * - [AudioContentBuilder] - For audio content (base64-encoded) + * + * @see TextContentBuilder + * @see ImageContentBuilder + * @see AudioContentBuilder + */ +@McpDsl +public abstract class MediaContentBuilder { + protected var annotations: Annotations? = null + protected var meta: JsonObject? = null + + /** + * Sets optional annotations for the content. + * + * Annotations provide additional metadata about the content such as audience, + * priority, and last modification time. + * + * Example: + * ```kotlin + * userText { + * text = "Hello" + * annotations(Annotations(priority = 0.8)) + * } + * ``` + * + * @param annotations The [Annotations] instance + */ + public fun annotations(annotations: Annotations) { + this.annotations = annotations + } + + /** + * Sets optional annotations for the content with individual parameters. + * + * Example with all parameters: + * ```kotlin + * userText { + * text = "Important update" + * annotations( + * audience = listOf(Role.User, Role.Assistant), + * priority = 0.8, + * lastModified = "2025-01-12T15:00:58Z" + * ) + * } + * ``` + * + * @param audience The intended audience for this content (list of [Role]) + * @param priority Priority hint for this content (0.0 to 1.0) + * @param lastModified ISO 8601 timestamp of the last modification + * @see Annotations + */ + public fun annotations(audience: List? = null, priority: Double? = null, lastModified: String? = null) { + this.annotations = Annotations(audience, priority, lastModified) + } + + /** + * Sets optional metadata for the content using a DSL builder. + * + * Example: + * ```kotlin + * userText { + * text = "Hello" + * meta { + * put("source", JsonPrimitive("user-input")) + * } + * } + * ``` + * + * @param block Lambda for building the metadata + */ + public fun meta(block: JsonObjectBuilder.() -> Unit) { + meta = buildJsonObject(block) + } + + /** Constructs the [MediaContent] instance from the current builder state. */ + @PublishedApi + internal abstract fun build(): MediaContent +} + +/** + * DSL builder for constructing [TextContent] instances. + * + * ## Required + * - [text] - The text content string + * + * ## Optional + * - [annotations] - Content annotations (inherited from [MediaContentBuilder]) + * - [meta] - Additional metadata (inherited from [MediaContentBuilder]) + * + * Example usage in sampling messages: + * ```kotlin + * createMessageRequest { + * maxTokens = 100 + * messages { + * userText { + * text = "What is the capital of France?" + * } + * } + * } + * ``` + * + * Example with annotations: + * ```kotlin + * userText { + * text = "Important message" + * annotations(Annotations(priority = 1.0)) + * } + * ``` + * + * @see TextContent + * @see MediaContentBuilder + */ +@McpDsl +public class TextContentBuilder @PublishedApi internal constructor() : MediaContentBuilder() { + /** + * The text content. This is a required field. + */ + public var text: String? = null + + override fun build(): TextContent { + val text = requireNotNull(text) { + "Missing required field 'text'. Example: text = \"Hello, world!\"" + } + + return TextContent(text = text, annotations = annotations, meta = meta) + } +} + +/** + * DSL builder for constructing [ImageContent] instances. + * + * ## Required + * - [data] - The base64-encoded image data + * - [mimeType] - The MIME type of the image (e.g., "image/png", "image/jpeg") + * + * ## Optional + * - [annotations] - Content annotations (inherited from [MediaContentBuilder]) + * - [meta] - Additional metadata (inherited from [MediaContentBuilder]) + * + * Example usage in sampling messages: + * ```kotlin + * createMessageRequest { + * maxTokens = 100 + * messages { + * userImage { + * data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB..." + * mimeType = "image/png" + * } + * } + * } + * ``` + * + * Example with annotations: + * ```kotlin + * userImage { + * data = base64EncodedImageData + * mimeType = "image/jpeg" + * annotations(Annotations(priority = 0.9)) + * } + * ``` + * + * @see ImageContent + * @see MediaContentBuilder + */ +@McpDsl +public class ImageContentBuilder @PublishedApi internal constructor() : MediaContentBuilder() { + /** + * The base64-encoded image data. This is a required field. + */ + public var data: String? = null + + /** + * The MIME type of the image (e.g., "image/png", "image/jpeg"). This is a required field. + */ + public var mimeType: String? = null + + override fun build(): ImageContent { + val data = requireNotNull(data) { + "Missing required field 'data'. Provide base64-encoded image data" + } + val mime = requireNotNull(mimeType) { + "Missing required field 'mimeType'. Example: mimeType = \"image/png\"" + } + + return ImageContent( + data = data, + mimeType = mime, + annotations = annotations, + meta = meta, + ) + } +} + +/** + * DSL builder for constructing [AudioContent] instances. + * + * ## Required + * - [data] - The base64-encoded audio data + * - [mimeType] - The MIME type of the audio (e.g., "audio/wav", "audio/mpeg") + * + * ## Optional + * - [annotations] - Content annotations (inherited from [MediaContentBuilder]) + * - [meta] - Additional metadata (inherited from [MediaContentBuilder]) + * + * Example usage in sampling messages: + * ```kotlin + * createMessageRequest { + * maxTokens = 100 + * messages { + * userAudio { + * data = "UklGRiQAAABXQVZFZm10IBAAAAABAAEA..." + * mimeType = "audio/wav" + * } + * } + * } + * ``` + * + * Example with annotations: + * ```kotlin + * userAudio { + * data = base64EncodedAudioData + * mimeType = "audio/mpeg" + * annotations(Annotations(priority = 0.7)) + * } + * ``` + * + * @see AudioContent + * @see MediaContentBuilder + */ +@McpDsl +public class AudioContentBuilder @PublishedApi internal constructor() : MediaContentBuilder() { + /** + * The base64-encoded audio data. This is a required field. + */ + public var data: String? = null + + /** + * The MIME type of the audio (e.g., "audio/wav", "audio/mpeg"). This is a required field. + */ + public var mimeType: String? = null + + override fun build(): AudioContent { + val data = requireNotNull(data) { + "Missing required field 'data'. Provide base64-encoded audio data" + } + val mime = requireNotNull(mimeType) { + "Missing required field 'mimeType'. Example: mimeType = \"audio/wav\"" + } + + return AudioContent( + data = data, + mimeType = mime, + annotations = annotations, + meta = meta, + ) + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.kt new file mode 100644 index 0000000..815e5f6 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.kt @@ -0,0 +1,245 @@ +@file:OptIn(ExperimentalSerializationApi::class) + +package io.modelcontextprotocol.kotlin.sdk.types + +import kotlinx.serialization.EncodeDefault +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject + +/** + * Discriminator values for the polymorphic [ContentBlock] hierarchy. + * + * @property value serialized string representation of this content type + */ +@Serializable +public enum class ContentTypes(public val value: String) { + @SerialName("text") + TEXT("text"), + + @SerialName("image") + IMAGE("image"), + + @SerialName("audio") + AUDIO("audio"), + + @SerialName("resource_link") + RESOURCE_LINK("resource_link"), + + @SerialName("resource") + EMBEDDED_RESOURCE("resource"), + + @SerialName("tool_use") + TOOL_USE("tool_use"), + + @SerialName("tool_result") + TOOL_RESULT("tool_result"), +} + +/** + * Base interface for all content blocks in the protocol. + */ +@Serializable(with = ContentBlockPolymorphicSerializer::class) +public sealed interface ContentBlock : WithMeta { + /** Discriminator identifying the content block subtype. */ + public val type: ContentTypes +} + +/** + * Content block that carries media data such as text, images, or audio. + * + * Every [MediaContent] is also a valid [SamplingMessageContent]; the sampling content + * hierarchy additionally admits [ToolUseContent] and [ToolResultContent]. + */ +@Serializable(with = MediaContentPolymorphicSerializer::class) +public sealed interface MediaContent : + ContentBlock, + SamplingMessageContent + +/** + * Content block that can appear inside a [SamplingMessage] or [CreateMessageResult]. + * + * Implemented by [TextContent], [ImageContent], [AudioContent], [ToolUseContent], + * and [ToolResultContent]. + */ +@Serializable(with = SamplingMessageContentPolymorphicSerializer::class) +public sealed interface SamplingMessageContent : WithMeta { + /** discriminator identifying the content block subtype */ + public val type: ContentTypes +} + +/** + * Text provided to or from an LLM. + * + * @property text The text content of the message. + * @property annotations Optional annotations for the client. + * @property meta property/parameter is reserved by MCP to allow clients and servers + * to attach additional metadata to their interactions. + */ +@Serializable +public data class TextContent( + val text: String, + val annotations: Annotations? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : MediaContent { + @EncodeDefault + public override val type: ContentTypes = ContentTypes.TEXT +} + +/** + * An image provided to or from an LLM. + * + * @property data The base64-encoded image data. + * @property mimeType The MIME type of the image. Different providers may support different image types. + * @property annotations Optional annotations for the client. + * @property meta property/parameter is reserved by MCP to allow clients and servers + * to attach additional metadata to their interactions. + */ +@Serializable +public data class ImageContent( + val data: String, + val mimeType: String, + val annotations: Annotations? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : MediaContent { + @EncodeDefault + public override val type: ContentTypes = ContentTypes.IMAGE +} + +/** + * Audio provided to or from an LLM. + * + * @property data The base64-encoded audio data. + * @property mimeType The MIME type of the audio. Different providers may support different audio types. + * @property annotations Optional annotations for the client. + * @property meta property/parameter is reserved by MCP to allow clients and servers + * to attach additional metadata to their interactions. + */ +@Serializable +public data class AudioContent( + val data: String, + val mimeType: String, + val annotations: Annotations? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : MediaContent { + @EncodeDefault + public override val type: ContentTypes = ContentTypes.AUDIO +} + +/** + * A resource that the server is capable of reading, included in a prompt or tool call result. + * + * Note: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests. + * + * @property name Intended for programmatic or logical use + * but used as a display name in past specs or fallback (if the title isn’t present). + * @property uri The URI of this resource. + * @property title Intended for UI and end-user contexts — optimized to be human-readable and easily understood, + * even by those unfamiliar with domain-specific terminology. + * + * If not provided, the name should be used for display + * (except for Tool, where `annotations.title` should be given precedence over using `name`, if present). + * @property size The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), + * if known. + * + * Hosts can use this to display file sizes and estimate context window usage. + * @property mimeType The MIME type of this resource, if known. + * @property icons Optional set of sized icons that clients can display in their user interface. + * See [Icon] for supported formats and requirements. + * @property description A description of what this resource represents. + * + * Clients can use this to improve the LLM’s understanding of available resources. + * It can be thought of as a "hint" to the model. + * @property annotations Optional annotations for the client. + * @property meta property/parameter is reserved by MCP to allow clients and servers + * to attach additional metadata to their interactions. + */ +@Serializable +public data class ResourceLink( + val name: String, + val uri: String, + val title: String? = null, + val size: Long? = null, + val mimeType: String? = null, + val icons: List? = null, + val description: String? = null, + val annotations: Annotations? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : ContentBlock, + ResourceLike { + @EncodeDefault + public override val type: ContentTypes = ContentTypes.RESOURCE_LINK +} + +/** + * The contents of a resource, embedded into a prompt or tool call result. + * + * It is up to the client how best to render embedded resources for the benefit of the LLM and/or the user. + * + * @property resource The resource contents. + * @property annotations Optional annotations for the client. + * @property meta property/parameter is reserved by MCP to allow clients and servers + * to attach additional metadata to their interactions. + */ +@Serializable +public data class EmbeddedResource( + val resource: ResourceContents, + val annotations: Annotations? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : ContentBlock { + @EncodeDefault + public override val type: ContentTypes = ContentTypes.EMBEDDED_RESOURCE +} + +/** + * A request from the assistant to invoke a tool during sampling. + * + * @property id Unique identifier for this tool use; matches a subsequent + * [ToolResultContent.toolUseId] that reports the result. + * @property name The tool name (must match a tool declared in the sampling request's tools list). + * @property input The arguments to pass to the tool, conforming to the tool's input schema. + * @property meta property/parameter is reserved by MCP to allow clients and servers + * to attach additional metadata to their interactions. + */ +@Serializable +public data class ToolUseContent( + val id: String, + val name: String, + val input: JsonObject, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : SamplingMessageContent { + @EncodeDefault + public override val type: ContentTypes = ContentTypes.TOOL_USE +} + +/** + * The result of a tool call previously requested via [ToolUseContent], supplied back + * to the assistant on the next sampling turn. + * + * @property toolUseId The id of the [ToolUseContent] this result corresponds to. + * @property content The unstructured result, following the same shape as [CallToolResult.content]. + * @property structuredContent Optional structured result; if the tool declared an output schema, + * this SHOULD conform to it. + * @property isError Whether the tool call ended in error. Defaults to absent (treated as false). + * @property meta property/parameter is reserved by MCP to allow clients and servers + * to attach additional metadata to their interactions. + */ +@Serializable +public data class ToolResultContent( + val toolUseId: String, + val content: List = emptyList(), + val structuredContent: JsonObject? = null, + val isError: Boolean? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : SamplingMessageContent { + @EncodeDefault + public override val type: ContentTypes = ContentTypes.TOOL_RESULT +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/discovery.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/discovery.kt new file mode 100644 index 0000000..d486b1f --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/discovery.kt @@ -0,0 +1,107 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import kotlinx.serialization.EncodeDefault +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Required +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.JsonObject + +/** + * Parameters for a `server/discover` request. + * + * Discovery belongs to the request-scoped lifecycle and therefore requires a protocol version and + * client capabilities in [meta]. Client information is recommended by the protocol but optional. + * Unknown metadata entries are preserved by [RequestMeta]. + * + * @property meta request-scoped protocol and client metadata + * @throws IllegalArgumentException if required request-scoped metadata is absent + * @throws SerializationException if required metadata is malformed + */ +@Serializable +@ExperimentalMcpApi +public data class DiscoverRequestParams( + @SerialName("_meta") + override val meta: RequestMeta, +) : RequestParams { + init { + requireNotNull(meta.protocolVersion) { "${RequestMetaKeys.PROTOCOL_VERSION} is required" } + requireNotNull(meta.clientCapabilities) { "${RequestMetaKeys.CLIENT_CAPABILITIES} is required" } + } +} + +/** + * Requests the protocol versions, capabilities, and metadata advertised by a server. + * + * @property params request metadata required by the request-scoped lifecycle + */ +@Serializable +@ExperimentalMcpApi +public data class DiscoverRequest(override val params: DiscoverRequestParams) : ClientRequest { + @OptIn(ExperimentalSerializationApi::class) + @EncodeDefault + override val method: Method = Method.Defined.ServerDiscover + + /** Metadata for this request. */ + public val meta: RequestMeta + get() = params.meta +} + +/** Defines where a cached discovery result may be reused. */ +@Serializable +@ExperimentalMcpApi +public enum class CacheScope { + /** The result may only be reused within the same authorization context. */ + @SerialName("private") + Private, + + /** The result may be shared across authorization contexts. */ + @SerialName("public") + Public, +} + +/** + * Result returned by `server/discover`. + * + * @property supportedVersions protocol versions supported by the server + * @property capabilities capabilities advertised by the server + * @property instructions optional guidance for clients using the server + * @property resultType discriminator for the result representation + * @property ttlMs number of milliseconds clients may treat this result as fresh + * @property cacheScope authorization boundary within which the result may be reused + * @property meta optional response metadata, including optional server information + * @throws IllegalArgumentException if [ttlMs] is negative + */ +@Serializable +@ExperimentalMcpApi +public data class DiscoverResult( + val supportedVersions: List, + val capabilities: ServerCapabilities, + val instructions: String? = null, + @EncodeDefault + val resultType: String = COMPLETE_RESULT_TYPE, + @Required + val ttlMs: Long = 0, + @Required + val cacheScope: CacheScope = CacheScope.Private, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : ServerResult { + init { + require(ttlMs >= 0) { "ttlMs must be non-negative, but was $ttlMs" } + } +} + +/** + * Data returned when a request selects an unsupported protocol version. + * + * @property supported protocol versions supported by the receiver + * @property requested protocol version selected by the request + */ +@Serializable +@ExperimentalMcpApi +public data class UnsupportedProtocolVersionData(val supported: List, val requested: String) + +internal const val COMPLETE_RESULT_TYPE: String = "complete" diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.dsl.kt new file mode 100644 index 0000000..12309fa --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.dsl.kt @@ -0,0 +1,232 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonObjectBuilder +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.decodeFromJsonElement +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * Creates an [ElicitRequest] using a type-safe DSL builder. + * + * ## Required + * - [message][ElicitRequestBuilder.message] - The message to present to the user + * - [requestedSchema][ElicitRequestBuilder.requestedSchema] - Schema defining the structure of requested data + * + * ## Optional + * - [meta][ElicitRequestBuilder.meta] - Metadata for the request + * + * Example requesting user information: + * ```kotlin + * val request = buildElicitRequest { + * message = "Please provide your contact information" + * requestedSchema { + * properties { + * put("email", JsonObject(mapOf( + * "type" to JsonPrimitive("string"), + * "description" to JsonPrimitive("Your email address") + * ))) + * put("name", JsonObject(mapOf( + * "type" to JsonPrimitive("string") + * ))) + * } + * required = listOf("email") + * } + * } + * ``` + * + * Example with simple text input: + * ```kotlin + * val request = buildElicitRequest { + * message = "Enter a project name" + * requestedSchema { + * properties { + * put("projectName", JsonObject(mapOf( + * "type" to JsonPrimitive("string"), + * "description" to JsonPrimitive("Name for the new project") + * ))) + * } + * } + * } + * ``` + * + * @param block Configuration lambda for setting up the elicitation request + * @return A configured [ElicitRequest] instance + * @see ElicitRequestBuilder + * @see ElicitRequest + */ +@OptIn(ExperimentalContracts::class) +@ExperimentalMcpApi +public inline fun buildElicitRequest(block: ElicitRequestBuilder.() -> Unit): ElicitRequest { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + return ElicitRequestBuilder().apply(block).build() +} + +/** + * DSL builder for constructing [ElicitRequest] instances. + * + * This builder is used to create requests that prompt users for structured input + * through forms or dialogs presented by the client. + * + * ## Required + * - [message] - The message to present to the user explaining what is being requested + * - [requestedSchema] - Schema defining the structure of the data to collect + * + * ## Optional + * - [meta] - Metadata for the request + * + * @see buildElicitRequest + * @see ElicitRequest + */ +@McpDsl +public class ElicitRequestBuilder @PublishedApi internal constructor() : RequestBuilder() { + /** + * The message to present to the user. This should clearly explain what information + * is being requested and why. This is a required field. + */ + public var message: String? = null + + private var requestedSchema: ElicitRequestParams.RequestedSchema? = null + + /** + * Sets the requested schema directly. + * + * Example: + * ```kotlin + * buildElicitRequest { + * message = "Enter details" + * requestedSchema(ElicitRequestParams.RequestedSchema( + * properties = buildJsonObject { + * put("name", JsonObject(mapOf("type" to JsonPrimitive("string")))) + * } + * )) + * } + * ``` + * + * @param schema The [ElicitRequestParams.RequestedSchema] instance + */ + public fun requestedSchema(schema: ElicitRequestParams.RequestedSchema) { + requestedSchema = schema + } + + /** + * Sets the requested schema using a DSL builder. + * + * This is the recommended way to define schemas. The schema defines the structure + * of data to be collected from the user, supporting only top-level primitive properties. + * + * Example: + * ```kotlin + * buildElicitRequest { + * message = "Configure settings" + * requestedSchema { + * properties { + * put("enabled", JsonObject(mapOf( + * "type" to JsonPrimitive("boolean"), + * "description" to JsonPrimitive("Enable feature") + * ))) + * put("apiKey", JsonObject(mapOf( + * "type" to JsonPrimitive("string") + * ))) + * } + * required = listOf("apiKey") + * } + * } + * ``` + * + * @param block Lambda for building the schema + * @see ElicitRequestedSchemaBuilder + */ + public fun requestedSchema(block: ElicitRequestedSchemaBuilder.() -> Unit) { + requestedSchema = ElicitRequestedSchemaBuilder().apply(block).build() + } + + @PublishedApi + override fun build(): ElicitRequest { + val message = requireNotNull(message) { + "Missing required field 'message'. Example: message = \"Please enter your name\"" + } + val requestedSchema = requireNotNull(requestedSchema) { + "Missing required field 'requestedSchema'. Use requestedSchema { properties { ... } }" + } + + val params = ElicitRequestParams(message = message, requestedSchema = requestedSchema, meta = meta) + return ElicitRequest(params) + } +} + +/** + * DSL builder for constructing [ElicitRequestParams.RequestedSchema] instances. + * + * Defines the JSON Schema structure for data to be collected from the user. + * Only supports top-level primitive properties. + * + * ## Required + * - [properties] - Schema definitions for each field + * + * ## Optional + * - [required] - List of required property names + * + * Example: + * ```kotlin + * requestedSchema { + * properties { + * put("username", JsonObject(mapOf( + * "type" to JsonPrimitive("string"), + * "description" to JsonPrimitive("Your username") + * ))) + * } + * required = listOf("username") + * } + * ``` + * + * @see ElicitRequestParams.RequestedSchema + */ +@McpDsl +public class ElicitRequestedSchemaBuilder @PublishedApi internal constructor() { + private var properties: JsonObject? = null + + /** + * List of required property names. If null, all fields are optional. + */ + public var required: List? = null + + /** + * Sets the schema properties directly. + * + * @param properties JsonObject containing property schemas + */ + public fun properties(properties: JsonObject) { + this.properties = properties + } + + /** + * Sets the schema properties using a DSL builder. + * + * Example: + * ```kotlin + * properties { + * put("email", JsonObject(mapOf( + * "type" to JsonPrimitive("string") + * ))) + * } + * ``` + * + * @param block Lambda for building the properties + */ + public fun properties(block: JsonObjectBuilder.() -> Unit): Unit = properties(buildJsonObject(block)) + + @PublishedApi + internal fun build(): ElicitRequestParams.RequestedSchema { + val properties = requireNotNull(properties) { + "Missing required field 'properties'. Use properties { put(\"fieldName\", schema) }" + } + val typedProperties: Map = properties.mapValues { (_, value) -> + McpJson.decodeFromJsonElement(value) + } + return ElicitRequestParams.RequestedSchema(properties = typedProperties, required = required) + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.kt new file mode 100644 index 0000000..1bb73e6 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.kt @@ -0,0 +1,204 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import kotlinx.serialization.EncodeDefault +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject + +/** + * Represents an `elicitation/create` request from the server to the client. + * + * Supports two modes: form mode ([ElicitRequestFormParams]) for collecting structured data + * in-band, and URL mode ([ElicitRequestURLParams]) for directing the user to an external URL. + * + * @property params The elicitation parameters — either form or URL mode. + */ +@Serializable +public data class ElicitRequest(override val params: ElicitRequestParams) : ServerRequest { + @EncodeDefault + public override val method: Method = Method.Defined.ElicitationCreate + + /** + * The message to present to the user. This should clearly explain what information is being requested and why. + */ + public val message: String + get() = params.message + + /** + * A restricted subset of JSON Schema defining the structure of the requested data. + */ + @Deprecated( + "Use (params as ElicitRequestFormParams).requestedSchema", + ReplaceWith("(params as ElicitRequestFormParams).requestedSchema"), + DeprecationLevel.WARNING, + ) + public val requestedSchema: ElicitRequestParams.RequestedSchema? + get() = (params as? ElicitRequestFormParams)?.requestedSchema + + /** + * Metadata for this request. May include a progressToken for out-of-band progress notifications. + */ + public val meta: RequestMeta? + get() = params.meta +} + +/** + * Represents the parameters for an `elicitation/create` request. + * + * Implementations: [ElicitRequestFormParams], [ElicitRequestURLParams]. + */ +@Serializable(with = ElicitRequestParamsSerializer::class) +public sealed interface ElicitRequestParams : RequestParams { + /** The message to present to the user describing what information is being requested. */ + public val message: String + + /** + * A restricted JSON Schema for elicitation requests. + * + * Only supports top-level primitive properties without nesting. Each property + * represents a field in the form or dialog presented to the user. + * + * @property schema Optional URI to a JSON Schema definition. + * @property properties A map of property names to their schema definitions. + * Each property must be a primitive type (string, number, boolean). + * @property required Optional list of property names that must be provided by the user. + * If omitted, all fields are considered optional. + */ + @Serializable + public data class RequestedSchema( + @SerialName($$"$schema") + val schema: String? = null, + val properties: Map, + val required: List? = null, + ) { + /** Always "object" for elicitation schemas. */ + @EncodeDefault + val type: String = "object" + } +} + +/** + * Creates an [ElicitRequestFormParams] for backwards compatibility. + * + * @param message The message to present to the user. + * @param requestedSchema The JSON Schema for the requested data. + * @param meta Optional request metadata. + * @return A configured [ElicitRequestFormParams] instance. + */ +@Deprecated( + "Use ElicitRequestFormParams instead", + ReplaceWith("ElicitRequestFormParams(message, requestedSchema = requestedSchema, meta = meta)"), + DeprecationLevel.WARNING, +) +@Suppress("FunctionName") +public fun ElicitRequestParams( + message: String, + requestedSchema: ElicitRequestParams.RequestedSchema, + meta: RequestMeta? = null, +): ElicitRequestFormParams = ElicitRequestFormParams( + message = message, + requestedSchema = requestedSchema, + meta = meta, +) + +/** + * Represents form mode parameters for an `elicitation/create` request. + * + * Collects non-sensitive structured data from the user via a form presented by the client. + * + * @property message The message to present to the user describing what information is being requested. + * @property task If specified, the caller is requesting task-augmented execution. The request + * will return a [CreateTaskResult] immediately, and the actual result can be retrieved + * later via `tasks/result`. + * @property requestedSchema A restricted subset of JSON Schema. Only top-level properties + * are allowed, without nesting. + * @property meta Optional metadata. May include a progressToken for out-of-band progress notifications. + */ +@Serializable +public data class ElicitRequestFormParams( + override val message: String, + val task: TaskMetadata? = null, + val requestedSchema: ElicitRequestParams.RequestedSchema, + @SerialName("_meta") + override val meta: RequestMeta? = null, +) : ElicitRequestParams { + /** The elicitation mode discriminator, always `"form"`. */ + @EncodeDefault + public val mode: String = "form" +} + +/** + * Represents URL mode parameters for an `elicitation/create` request. + * + * Directs the user to an external URL for out-of-band interactions (e.g., OAuth flows, + * payment processing, or entering sensitive credentials) that must not pass through the MCP client. + * + * Handling the [url] is the responsibility of the host application: it should require HTTPS, clearly + * display the target domain, obtain explicit user consent, and open the URL in a secure browser context. + * The SDK neither opens, fetches, nor validates the URL. URLs must only appear in URL-mode requests — + * never render a URL from a form-mode request as a clickable link. + * + * @property message The message explaining why the interaction is needed. + * @property elicitationId A unique identifier for this elicitation. The client MUST treat + * this ID as an opaque value. + * @property url The URL that the user should navigate to. + * @property task If specified, the caller is requesting task-augmented execution. The request + * will return a [CreateTaskResult] immediately, and the actual result can be retrieved + * later via `tasks/result`. + * @property meta Optional metadata. May include a progressToken for out-of-band progress notifications. + */ +@Serializable +public data class ElicitRequestURLParams( + override val message: String, + val elicitationId: String, + val url: String, + val task: TaskMetadata? = null, + @SerialName("_meta") + override val meta: RequestMeta? = null, +) : ElicitRequestParams { + /** The elicitation mode discriminator, always `"url"`. */ + @EncodeDefault + public val mode: String = "url" +} + +/** + * Represents the client's response to an [ElicitRequest]. + * + * @property action The user action in response to the elicitation. + * @property content The submitted form data, only present when [action] is [Action.Accept] + * and mode was form. Contains values matching the requested schema. Omitted for + * URL mode responses. + * @property meta Optional metadata for this response. + */ +@Serializable +public data class ElicitResult( + val action: Action, + val content: JsonObject? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : ClientResult { + + init { + require(action == Action.Accept || content == null) { + "Content can only be provided when action is 'accept', got action=$action with content" + } + } + + /** + * The user's response action to an elicitation request. + */ + @Serializable + public enum class Action { + /** User submitted the form/confirmed the action */ + @SerialName("accept") + Accept, + + /** User explicitly declined the action */ + @SerialName("decline") + Decline, + + /** User dismissed without making an explicit choice */ + @SerialName("cancel") + Cancel, + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/initialize.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/initialize.dsl.kt new file mode 100644 index 0000000..f3e1575 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/initialize.dsl.kt @@ -0,0 +1,206 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * Creates an [InitializeRequest] using a type-safe DSL builder. + * + * ## Required + * - [protocolVersion][InitializeRequestBuilder.protocolVersion] - MCP protocol version + * - [capabilities][InitializeRequestBuilder.capabilities] - Client capabilities + * - [info][InitializeRequestBuilder.info] - Client implementation information + * + * ## Optional + * - [meta][InitializeRequestBuilder.meta] - Metadata for the request + * + * Example: + * ```kotlin + * val request = buildInitializeRequest { + * protocolVersion = "2024-11-05" + * capabilities { + * sampling(ClientCapabilities.Sampling()) + * roots(listChanged = true) + * } + * info("MyClient", "1.0.0") + * } + * ``` + * + * Example with full client info: + * ```kotlin + * val request = buildInitializeRequest { + * protocolVersion = "2024-11-05" + * capabilities { + * sampling(ClientCapabilities.Sampling()) + * experimental { + * put("feature", JsonPrimitive(true)) + * } + * } + * info( + * name = "MyAdvancedClient", + * version = "2.0.0", + * title = "Advanced MCP Client", + * websiteUrl = "https://example.com" + * ) + * } + * ``` + * + * @param block Configuration lambda for setting up the initialize request + * @return A configured [InitializeRequest] instance + * @see InitializeRequestBuilder + * @see InitializeRequest + */ +@OptIn(ExperimentalContracts::class) +@ExperimentalMcpApi +public inline fun buildInitializeRequest(block: InitializeRequestBuilder.() -> Unit): InitializeRequest { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + return InitializeRequestBuilder().apply(block).build() +} + +/** + * DSL builder for constructing [InitializeRequest] instances. + * + * This builder creates the initial handshake request to establish + * an MCP protocol connection between client and server. + * + * ## Required + * - [protocolVersion] - MCP protocol version string + * - [capabilities] - Client capabilities configuration + * - [info] - Client implementation details + * + * ## Optional + * - [meta] - Metadata for the request + * + * @see buildInitializeRequest + * @see InitializeRequest + */ +@McpDsl +public class InitializeRequestBuilder @PublishedApi internal constructor() : RequestBuilder() { + /** + * The MCP protocol version. This is a required field. + * + * Example: `protocolVersion = "2024-11-05"` + */ + public var protocolVersion: String? = null + + private var capabilities: ClientCapabilities? = null + private var info: Implementation? = null + + /** + * Sets client capabilities directly. + * + * Example: + * ```kotlin + * capabilities(ClientCapabilities( + * sampling = ClientCapabilities.Sampling(), + * roots = ClientCapabilities.Roots(listChanged = true) + * )) + * ``` + * + * @param value The [ClientCapabilities] instance + */ + public fun capabilities(value: ClientCapabilities) { + capabilities = value + } + + /** + * Sets client capabilities using a DSL builder. + * + * This is the recommended way to configure capabilities. + * + * Example: + * ```kotlin + * capabilities { + * sampling(ClientCapabilities.Sampling()) + * roots(listChanged = true) + * elicitation(ClientCapabilities.elicitation) + * } + * ``` + * + * @param block Lambda for building capabilities + * @see ClientCapabilitiesBuilder + */ + public fun capabilities(block: ClientCapabilitiesBuilder.() -> Unit) { + capabilities = ClientCapabilitiesBuilder().apply(block).build() + } + + /** + * Sets client implementation info directly. + * + * Example: + * ```kotlin + * info(Implementation( + * name = "MyClient", + * version = "1.0.0" + * )) + * ``` + * + * @param value The [Implementation] instance + */ + public fun info(value: Implementation) { + info = value + } + + /** + * Sets client implementation info using individual parameters. + * + * This is the recommended way to configure client info. + * + * Example with required fields only: + * ```kotlin + * info("MyClient", "1.0.0") + * ``` + * + * Example with all fields: + * ```kotlin + * info( + * name = "MyClient", + * version = "2.0.0", + * title = "My MCP Client", + * websiteUrl = "https://example.com", + * icons = listOf(Icon( + * src = "https://example.com/icon.png", + * mimeType = "image/png" + * )) + * ) + * ``` + * + * @param name Client implementation name + * @param version Client version string + * @param title Optional human-readable title + * @param websiteUrl Optional URL to client website + * @param icons Optional list of client icons + */ + public fun info( + name: String, + version: String, + title: String? = null, + websiteUrl: String? = null, + icons: List? = null, + ) { + info = Implementation(name = name, version = version, title = title, websiteUrl = websiteUrl, icons = icons) + } + + @PublishedApi + override fun build(): InitializeRequest { + val version = requireNotNull(protocolVersion) { + "Missing required field 'protocolVersion'. Example: protocolVersion = \"2024-11-05\"" + } + val capabilities = requireNotNull(capabilities) { + "Missing required field 'capabilities'. Use capabilities { ... }" + } + val info = requireNotNull(info) { + "Missing required field 'info'. Use info(\"clientName\", \"1.0.0\")" + } + + val params = InitializeRequestParams( + protocolVersion = version, + capabilities = capabilities, + clientInfo = info, + meta = meta, + ) + return InitializeRequest(params = params) + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/initialize.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/initialize.kt new file mode 100644 index 0000000..6c69112 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/initialize.kt @@ -0,0 +1,98 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import kotlinx.serialization.EncodeDefault +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject + +/** + * This request is sent from the client to the server when it first connects, + * asking it to begin initialization. + * + * The initialize request is the first message exchanged in the MCP protocol handshake. + * It allows the client to communicate its capabilities and protocol version to the server, + * and receive the server's capabilities in response. + * + * @property params The initialization parameters including protocol version and client capabilities. + */ +@Serializable +public data class InitializeRequest(override val params: InitializeRequestParams) : ClientRequest { + @OptIn(ExperimentalSerializationApi::class) + @EncodeDefault + override val method: Method = Method.Defined.Initialize + + /** + * The latest version of the Model Context Protocol that the client supports. + */ + public val protocolVersion: String + get() = params.protocolVersion + + /** + * The capabilities that this client supports. Describes which optional features the client has implemented. + */ + public val capabilities: ClientCapabilities + get() = params.capabilities + + /** + * Information about the client implementation, including name, version, and branding. + */ + public val clientInfo: Implementation + get() = params.clientInfo + + /** + * Metadata for this request. May include a progressToken for out-of-band progress notifications. + */ + public val meta: RequestMeta? + get() = params.meta +} + +/** + * Parameters for an initialize request. + * + * @property protocolVersion The latest version of the Model Context Protocol that the client supports. + * The client MAY decide to support older versions as well. + * The server will respond with its preferred version, which may differ. + * @property capabilities The capabilities that this client supports. + * Describes which optional features the client has implemented. + * @property clientInfo Information about the client implementation, including name, version, and branding. + * @property meta Optional metadata for this request. May include a progressToken for + * out-of-band progress notifications. + */ +@Serializable +public data class InitializeRequestParams( + val protocolVersion: String, + val capabilities: ClientCapabilities, + val clientInfo: Implementation, + @SerialName("_meta") + override val meta: RequestMeta? = null, +) : RequestParams + +/** + * After receiving an [InitializeRequest] from the client, the server sends this response. + * + * The server communicates its chosen protocol version, capabilities, and optional + * instructions for how to effectively use its features. + * + * @property protocolVersion The version of the Model Context Protocol that the server wants to use. + * This may not match the version that the client requested. + * If the client cannot support this version, it MUST disconnect. + * @property capabilities The capabilities that this server supports. + * Describes which optional features the server has implemented. + * @property serverInfo Information about the server implementation, including name, version, and branding. + * @property instructions Optional instructions describing how to use the server and its features. + * Clients can use this to improve the LLM's understanding of available + * tools, resources, etc. It can be thought of as a "hint" to the model. + * For example, this information MAY be added to the system prompt to help + * the LLM make better use of the server's capabilities. + * @property meta Optional metadata for this response. + */ +@Serializable +public data class InitializeResult( + val protocolVersion: String = LATEST_PROTOCOL_VERSION, + val capabilities: ServerCapabilities, + val serverInfo: Implementation, + val instructions: String? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : ServerResult diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt new file mode 100644 index 0000000..7e10904 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt @@ -0,0 +1,299 @@ +@file:OptIn(ExperimentalAtomicApi::class, ExperimentalSerializationApi::class) + +package io.modelcontextprotocol.kotlin.sdk.types + +import kotlinx.serialization.EncodeDefault +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.encodeToJsonElement +import kotlin.concurrent.atomics.ExperimentalAtomicApi +import kotlin.jvm.JvmInline +import kotlin.jvm.JvmOverloads +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +/** JSON-RPC protocol version used by MCP (`"2.0"`). */ +public const val JSONRPC_VERSION: String = "2.0" + +/** + * Creates a `RequestId` instance using the provided string value. + * + * @param value The string representation of the request ID. This value is used + * to uniquely identify a request in the JSON-RPC context. + * @return A `RequestId` instance of type `StringId` with the provided string value. + */ +public fun RequestId(value: String): RequestId = RequestId.StringId(value) + +/** + * Creates a `RequestId` instance using the given numeric value. + * + * @param value The numeric value to be used for the `RequestId`. This should uniquely + * identify a request when represented as a `NumberId`. + * @return A `RequestId.NumberId` instance encapsulating the provided numeric value. + */ +public fun RequestId(value: Long): RequestId = RequestId.NumberId(value) + +/** + * A uniquely identifying ID for a request in JSON-RPC. + */ +@Serializable(with = RequestIdPolymorphicSerializer::class) +public sealed interface RequestId { + + /** + * A string-based request identifier. + * + * @property value the string representation of this request ID + */ + @JvmInline + @Serializable + public value class StringId(public val value: String) : RequestId + + /** + * A numeric request identifier. + * + * @property value the numeric representation of this request ID + */ + @JvmInline + @Serializable + public value class NumberId(public val value: Long) : RequestId +} + +/** + * Converts the request to a JSON-RPC request. + * + * @return The JSON-RPC request representation. + */ +public fun Request.toJSON(): JSONRPCRequest = JSONRPCRequest( + method = method.value, + params = this.params?.let { + McpJson.encodeToJsonElement(it) + }, +) + +/** + * Decodes a JSON-RPC request into a protocol-specific [Request]. + * + * @return The decoded [Request] + */ +public fun JSONRPCRequest.fromJSON(): Request = + McpJson.decodeFromJsonElement(McpJson.encodeToJsonElement(this)) + +/** + * Converts the notification to a JSON-RPC notification. + * + * @return The JSON-RPC notification representation. + */ +public fun Notification.toJSON(): JSONRPCNotification = JSONRPCNotification( + method = method.value, + params = this.params?.let { + McpJson.encodeToJsonElement(it) + }, +) + +/** + * Decodes a JSON-RPC notification into a protocol-specific [Notification]. + * + * @return The decoded [Notification]. + */ +internal fun JSONRPCNotification.fromJSON(): Notification = + McpJson.decodeFromJsonElement(McpJson.encodeToJsonElement(this)) + +/** + * Base interface for all JSON-RPC 2.0 messages. + * + * All messages in the MCP protocol follow the JSON-RPC 2.0 specification. + */ +@Serializable(with = JSONRPCMessagePolymorphicSerializer::class) +public sealed interface JSONRPCMessage { + /** The JSON-RPC protocol version, always `"2.0"`. */ + public val jsonrpc: String +} + +/** + * Represents an empty JSON-RPC message used as a placeholder or no-op response. + */ +@Serializable +public data object JSONRPCEmptyMessage : JSONRPCMessage { + override val jsonrpc: String = JSONRPC_VERSION +} + +// ============================================================================ +// JSONRPCRequest +// ============================================================================ + +/** + * A request that expects a response. + * + * Requests are identified by a unique [id] and specify a [method] to invoke. + * The server or client (depending on a direction) must respond with either a + * [JSONRPCResponse] or [JSONRPCError] that has the same [id]. + * + * @property id A unique identifier for this request. The response will include the same ID. + * Can be a string or number. UUID string is used by default. + * @property method The name of the method to invoke (e.g., "tools/list", "resources/read"). + * @property params Optional parameters for the method. Structure depends on the specific method. + */ +@Serializable +@OptIn(ExperimentalUuidApi::class) +public data class JSONRPCRequest @JvmOverloads public constructor( + val id: RequestId = RequestId(Uuid.random().toHexString()), + val method: String, + val params: JsonElement? = null, +) : JSONRPCMessage { + /** Always `"2.0"` to indicate JSON-RPC 2.0 protocol. */ + @EncodeDefault + override val jsonrpc: String = JSONRPC_VERSION + + /** + * Secondary constructor for creating a `JSONRPCRequest` using a string-based ID. + * + * @param id The string ID for the request. + * @param method The method name for the request. + * @param params The parameters for the request as a JSON element. Defaults to `null`. + */ + public constructor( + id: String, + method: String, + params: JsonElement? = null, + ) : this(id = RequestId.StringId(id), method = method, params = params) + + /** + * Constructs a JSON-RPC request using a numerical ID, method name, and optional parameters. + * + * @param id The numerical ID for the request. + * @param method The method name for the request. + * @param params The parameters for the request as a JSON element. Defaults to `null`. + */ + public constructor( + id: Long, + method: String, + params: JsonElement? = null, + ) : this(id = RequestId.NumberId(id), method = method, params = params) +} + +// ============================================================================ +// JSONRPCNotification +// ============================================================================ + +/** + * A notification which does not expect a response. + * + * Notifications are fire-and-forget messages. They do not have an `id` and + * the recipient does not send any response (neither success nor error). + * + * Examples: progress updates, resource change notifications, log messages. + * + * @property method The name of the notification method (e.g., "notifications/progress", + * "notifications/resources/updated"). + * @property params Optional parameters for the notification. Structure depends on the specific method. + */ +@Serializable +public data class JSONRPCNotification(val method: String, val params: JsonElement? = null) : JSONRPCMessage { + /** Always `"2.0"` to indicate JSON-RPC 2.0 protocol. */ + @EncodeDefault + override val jsonrpc: String = JSONRPC_VERSION +} + +// ============================================================================ +// JSONRPCResponse +// ============================================================================ + +/** + * A successful (non-error) response to a request. + * + * Sent in response to a [JSONRPCRequest] when the method execution succeeds. + * The [id] must match the [id] of the original request. + * + * @property id The identifier from the original request. Used to match responses to requests. + * @property result The result of the method execution. Structure depends on the method that was called. + */ +@Serializable +public data class JSONRPCResponse(val id: RequestId, val result: RequestResult = EmptyResult()) : JSONRPCMessage { + /** Always `"2.0"` to indicate JSON-RPC 2.0 protocol. */ + @EncodeDefault + override val jsonrpc: String = JSONRPC_VERSION +} + +// ============================================================================ +// JSONRPCError +// ============================================================================ + +/** + * A response to a request that indicates an error occurred. + * + * Sent in response to a [JSONRPCRequest] when the method execution fails. + * The [id] must match the [id] of the original request. + * + * @property id The identifier from the original request. Used to match error responses to requests. + * @property error Details about the error that occurred, including error code and message. + */ +@Serializable +public data class JSONRPCError(val id: RequestId?, val error: RPCError) : JSONRPCMessage { + /** Always `"2.0"` to indicate JSON-RPC 2.0 protocol. */ + @EncodeDefault + override val jsonrpc: String = JSONRPC_VERSION +} + +/** + * Error information for a failed JSON-RPC request. + * + * @property code The error type that occurred. A number indicating the error category. + * See standard JSON-RPC 2.0 error codes: + * - -32700: Parse error (invalid JSON) + * - -32600: Invalid request (not valid JSON-RPC) + * - -32601: Method not found + * - -32602: Invalid params + * - -32603: Internal error + * - -32000 to -32099: Server-defined errors + * @property message A short description of the error. + * The message SHOULD be limited to a concise single sentence. + * @property data Additional information about the error. + * The value of this member is defined by the sender + * (e.g., detailed error information, nested errors, etc.). + */ +@Serializable +public data class RPCError(val code: Int, val message: String, val data: JsonElement? = null) { + /** + * Standard JSON-RPC 2.0 and MCP SDK error codes. + */ + public object ErrorCode { + // SDK-specific error codes + /** Connection was closed */ + public const val CONNECTION_CLOSED: Int = -32000 + + /** Request timed out */ + public const val REQUEST_TIMEOUT: Int = -32001 + + /** Resource not found */ + public const val RESOURCE_NOT_FOUND: Int = -32002 + + /** The request selected a protocol version that the receiver does not support. */ + public const val UNSUPPORTED_PROTOCOL_VERSION: Int = -32022 + + /** + * A URL-mode elicitation must be completed before the request can be processed. + * The error [data][RPCError.data] carries the required URL-mode elicitations + * (see [UrlElicitationRequiredException]). + */ + public const val URL_ELICITATION_REQUIRED: Int = -32042 + + // Standard JSON-RPC 2.0 error codes + + /** Invalid JSON was received */ + public const val PARSE_ERROR: Int = -32700 + + /** The JSON sent is not a valid Request object */ + public const val INVALID_REQUEST: Int = -32600 + + /** The method does not exist or is not available */ + public const val METHOD_NOT_FOUND: Int = -32601 + + /** Invalid method parameter(s) */ + public const val INVALID_PARAMS: Int = -32602 + + /** Internal JSON-RPC error */ + public const val INTERNAL_ERROR: Int = -32603 + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonUtils.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonUtils.kt new file mode 100644 index 0000000..d83e012 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonUtils.kt @@ -0,0 +1,94 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.ClassDiscriminatorMode +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.add +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject + +/** Reusable empty [JsonObject] instance. */ +public val EmptyJsonObject: JsonObject = JsonObject(emptyMap()) + +/** Pre-configured [Json] instance for MCP serialization. */ +@OptIn(ExperimentalSerializationApi::class) +public val McpJson: Json by lazy { + Json { + ignoreUnknownKeys = true + encodeDefaults = true + isLenient = true + classDiscriminatorMode = ClassDiscriminatorMode.NONE + explicitNulls = false + } +} + +/** + * Converts a [Map] with [String] keys and nullable [Any] values into a [Map] with + * [String] keys and [JsonElement] values. + * + * Each value in the resulting map is derived from the corresponding value + * in the input map, transformed into a [JsonElement]. If the transformation + * fails for any reason, the value is replaced with a [JsonPrimitive] containing + * the original value's `toString` representation. + * + * @return A new map where the values are converted to [JsonElement] instances. + */ +public fun Map.toJson(): Map = this.mapValues { (_, value) -> + runCatching { convertToJsonElement(value) } + .getOrElse { JsonPrimitive(value.toString()) } +} + +@OptIn(ExperimentalUnsignedTypes::class, ExperimentalSerializationApi::class) +private fun convertToJsonElement(value: Any?): JsonElement = when (value) { + null -> JsonNull + + is JsonElement -> value + + is String -> JsonPrimitive(value) + + is Number -> JsonPrimitive(value) + + is Boolean -> JsonPrimitive(value) + + is Char -> JsonPrimitive(value.toString()) + + is Enum<*> -> JsonPrimitive(value.name) + + is Map<*, *> -> buildJsonObject { value.forEach { (k, v) -> put(k.toString(), convertToJsonElement(v)) } } + + is Collection<*> -> buildJsonArray { value.forEach { add(convertToJsonElement(it)) } } + + is Array<*> -> buildJsonArray { value.forEach { add(convertToJsonElement(it)) } } + + // Primitive arrays + is IntArray -> buildJsonArray { value.forEach { add(it) } } + + is LongArray -> buildJsonArray { value.forEach { add(it) } } + + is FloatArray -> buildJsonArray { value.forEach { add(it) } } + + is DoubleArray -> buildJsonArray { value.forEach { add(it) } } + + is BooleanArray -> buildJsonArray { value.forEach { add(it) } } + + is ShortArray -> buildJsonArray { value.forEach { add(it) } } + + is ByteArray -> buildJsonArray { value.forEach { add(it) } } + + is CharArray -> buildJsonArray { value.forEach { add(it.toString()) } } + + // Unsigned arrays + is UIntArray -> buildJsonArray { value.forEach { add(JsonPrimitive(it)) } } + + is ULongArray -> buildJsonArray { value.forEach { add(JsonPrimitive(it)) } } + + is UShortArray -> buildJsonArray { value.forEach { add(JsonPrimitive(it)) } } + + is UByteArray -> buildJsonArray { value.forEach { add(JsonPrimitive(it)) } } + + else -> JsonPrimitive(value.toString()) +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/logging.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/logging.dsl.kt new file mode 100644 index 0000000..b65846c --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/logging.dsl.kt @@ -0,0 +1,87 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * Creates a [SetLevelRequest] using a type-safe DSL builder. + * + * ## Required + * - [loggingLevel][SetLevelRequestBuilder.loggingLevel] - The logging level to set + * + * ## Optional + * - [meta][SetLevelRequestBuilder.meta] - Metadata for the request + * + * Example setting info level: + * ```kotlin + * val request = buildSetLevelRequest { + * loggingLevel = LoggingLevel.Info + * } + * ``` + * + * Example setting debug level: + * ```kotlin + * val request = buildSetLevelRequest { + * loggingLevel = LoggingLevel.Debug + * } + * ``` + * + * @param block Configuration lambda for setting up the logging level request + * @return A configured [SetLevelRequest] instance + * @see SetLevelRequestBuilder + * @see SetLevelRequest + * @see LoggingLevel + */ +@OptIn(ExperimentalContracts::class) +@ExperimentalMcpApi +public inline fun buildSetLevelRequest(block: SetLevelRequestBuilder.() -> Unit): SetLevelRequest { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + return SetLevelRequestBuilder().apply(block).build() +} + +/** + * DSL builder for constructing [SetLevelRequest] instances. + * + * This builder creates requests to set the logging level for the server. + * + * ## Required + * - [loggingLevel] - The logging level to set + * + * ## Optional + * - [meta] - Metadata for the request + * + * @see buildSetLevelRequest + * @see SetLevelRequest + * @see LoggingLevel + */ +@McpDsl +public class SetLevelRequestBuilder @PublishedApi internal constructor() : RequestBuilder() { + /** + * The logging level to set. This is a required field. + * + * Available levels (from least to most severe): + * - [LoggingLevel.Debug] + * - [LoggingLevel.Info] + * - [LoggingLevel.Notice] + * - [LoggingLevel.Warning] + * - [LoggingLevel.Error] + * - [LoggingLevel.Critical] + * - [LoggingLevel.Alert] + * - [LoggingLevel.Emergency] + * + * Example: `loggingLevel = LoggingLevel.Warning` + */ + public var loggingLevel: LoggingLevel? = null + + @PublishedApi + override fun build(): SetLevelRequest { + val level = requireNotNull(loggingLevel) { + "Missing required field 'loggingLevel'. Example: loggingLevel = LoggingLevel.Info" + } + + val params = SetLevelRequestParams(level = level, meta = meta) + return SetLevelRequest(params = params) + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/logging.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/logging.kt new file mode 100644 index 0000000..d1e5f00 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/logging.kt @@ -0,0 +1,96 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import kotlinx.serialization.EncodeDefault +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * The severity of a log message. + * + * These levels map to syslog message severities, as specified in + * [RFC-5424](https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1). + * + * Levels are ordered from least to most severe: + * [Debug] < [Info] < [Notice] < [Warning] < [Error] < [Critical] < [Alert] < [Emergency] + */ +@Serializable +public enum class LoggingLevel { + /** Detailed debug information for troubleshooting (RFC-5424: Debug) */ + @SerialName("debug") + Debug, + + /** Informational messages about normal operations (RFC-5424: Informational) */ + @SerialName("info") + Info, + + /** Normal but significant conditions (RFC-5424: Notice) */ + @SerialName("notice") + Notice, + + /** Warning conditions that may require attention (RFC-5424: Warning) */ + @SerialName("warning") + Warning, + + /** Error conditions that require attention (RFC-5424: Error) */ + @SerialName("error") + Error, + + /** Critical conditions requiring immediate action (RFC-5424: Critical) */ + @SerialName("critical") + Critical, + + /** Action must be taken immediately (RFC-5424: Alert) */ + @SerialName("alert") + Alert, + + /** System is unusable, highest severity (RFC-5424: Emergency) */ + @SerialName("emergency") + Emergency, +} + +/** + * A request from the client to the server to enable or adjust logging. + * + * After receiving this request, the server should send log messages at the specified + * [level][SetLevelRequestParams.level] and higher (more severe) to the client as + * notifications/message events. + * + * @property params The parameters specifying the desired logging level. + */ +@Serializable +public data class SetLevelRequest(override val params: SetLevelRequestParams) : ClientRequest { + @OptIn(ExperimentalSerializationApi::class) + @EncodeDefault + override val method: Method = Method.Defined.LoggingSetLevel + + /** + * The minimum severity level of logging that the client wants to receive from the server. + */ + public val level: LoggingLevel + get() = params.level + + /** + * Metadata for this request. May include a progressToken for out-of-band progress notifications. + */ + public val meta: RequestMeta? + get() = params.meta +} + +/** + * Parameters for a logging/setLevel request. + * + * @property level The minimum severity level of logging that the client wants to receive + * from the server. The server should send all logs at this level and higher + * (i.e., more severe) to the client as notifications/message. + * For example, if [level] is [LoggingLevel.Warning], the server should send + * warning, error, critical, alert, and emergency messages, but not info, notice, or debug. + * @property meta Optional metadata for this request. May include a progressToken for + * out-of-band progress notifications. + */ +@Serializable +public data class SetLevelRequestParams( + val level: LoggingLevel, + @SerialName("_meta") + override val meta: RequestMeta? = null, +) : RequestParams diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/methods.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/methods.kt new file mode 100644 index 0000000..1824ad0 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/methods.kt @@ -0,0 +1,66 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import kotlinx.serialization.Serializable + +/** + * Represents a method in the protocol, which can be predefined or custom. + */ +@Serializable(with = MethodSerializer::class) +public sealed interface Method { + /** The string representation of this method name. */ + public val value: String + + /** + * Enum of predefined methods supported by the protocol. + */ + @Serializable + public enum class Defined(override val value: String) : Method { + Initialize("initialize"), + ServerDiscover("server/discover"), + Ping("ping"), + ResourcesList("resources/list"), + ResourcesTemplatesList("resources/templates/list"), + ResourcesRead("resources/read"), + ResourcesSubscribe("resources/subscribe"), + ResourcesUnsubscribe("resources/unsubscribe"), + PromptsList("prompts/list"), + PromptsGet("prompts/get"), + NotificationsCancelled("notifications/cancelled"), + NotificationsInitialized("notifications/initialized"), + NotificationsProgress("notifications/progress"), + NotificationsMessage("notifications/message"), + NotificationsResourcesUpdated("notifications/resources/updated"), + NotificationsResourcesListChanged("notifications/resources/list_changed"), + NotificationsToolsListChanged("notifications/tools/list_changed"), + NotificationsRootsListChanged("notifications/roots/list_changed"), + NotificationsPromptsListChanged("notifications/prompts/list_changed"), + NotificationsElicitationComplete("notifications/elicitation/complete"), + NotificationsTasksStatus("notifications/tasks/status"), + ToolsList("tools/list"), + ToolsCall("tools/call"), + LoggingSetLevel("logging/setLevel"), + SamplingCreateMessage("sampling/createMessage"), + CompletionComplete("completion/complete"), + RootsList("roots/list"), + ElicitationCreate("elicitation/create"), + TasksGet("tasks/get"), + TasksResult("tasks/result"), + TasksList("tasks/list"), + TasksCancel("tasks/cancel"), + } + + /** + * Represents a custom method defined by the user. + */ + @Serializable + public data class Custom(override val value: String) : Method + + public companion object { + private val definedByValue: Map by lazy { + Defined.entries.associateBy { it.value } + } + + /** Resolves a wire method string to a [Defined] entry when known, [Custom] otherwise. */ + internal fun from(value: String): Method = definedByValue[value] ?: Custom(value) + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/notification.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/notification.kt new file mode 100644 index 0000000..3d33c21 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/notification.kt @@ -0,0 +1,427 @@ +@file:OptIn(ExperimentalSerializationApi::class) + +package io.modelcontextprotocol.kotlin.sdk.types + +import kotlinx.serialization.EncodeDefault +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject + +/** + * Represents a notification in the protocol. + */ +@Serializable(with = NotificationPolymorphicSerializer::class) +public sealed interface Notification { + /** The notification method identifier. */ + public val method: Method + + /** Optional notification parameters. */ + public val params: NotificationParams? +} + +/** + * Represents a notification sent by the client. + */ +@Serializable(with = ClientNotificationPolymorphicSerializer::class) +public sealed interface ClientNotification : Notification + +/** + * Represents a notification sent by the server. + */ +@Serializable(with = ServerNotificationPolymorphicSerializer::class) +public sealed interface ServerNotification : Notification + +/** + * Interface for notification parameter types. Inherits the optional `_meta` field from [WithMeta]. + */ +@Serializable +public sealed interface NotificationParams : WithMeta + +/** + * Base parameters for notifications that only contain metadata. + */ +@Serializable +public data class BaseNotificationParams(@SerialName("_meta") override val meta: JsonObject? = null) : + NotificationParams + +/** + * Represents a progress notification. + * + * @property progress The progress thus far. This should increase every time progress is made, + * even if the total is unknown. + * @property total Total number of items to a process (or total progress required), if known. + * @property message An optional message describing the current progress. + */ +@Serializable +public class Progress( + public val progress: Double, + public val total: Double? = null, + public val message: String? = null, +) + +// ============================================================================ +// Custom Notification +// ============================================================================ + +/** + * Represents a custom notification method that is not part of the core MCP specification. + * + * The MCP protocol allows implementations to define custom methods for extending functionality. + * This class captures such custom notifications while preserving all their data. + * + * @property method The custom method name. By convention, custom methods often contain + * organization-specific prefixes (e.g., "mycompany/custom_event"). + * @property params Raw JSON parameters for the custom notification, if present. + */ +@Serializable +public data class CustomNotification(override val method: Method, override val params: BaseNotificationParams? = null) : + ClientNotification, + ServerNotification { + + /** Convenience accessor for [params]'s metadata. */ + public val meta: JsonObject? + get() = params?.meta +} + +// ============================================================================ +// Cancelled Notification +// ============================================================================ + +/** + * This notification can be sent by either side to indicate that it is cancelling a previously-issued request. + * + * The request SHOULD still be in-flight, but due to communication latency, + * it is always possible that this notification MAY arrive after the request has already finished. + * + * This notification indicates that the result will be unused, so any associated processing SHOULD cease. + * + * A client MUST NOT attempt to cancel its `initialize` request. + * + * @property params Details of the cancellation request. + */ +@Serializable +public data class CancelledNotification(override val params: CancelledNotificationParams) : + ClientNotification, + ServerNotification { + @EncodeDefault + override val method: Method = Method.Defined.NotificationsCancelled + + /** + * The ID of the request to cancel. + */ + public val requestId: RequestId + get() = params.requestId + + /** + * A string describing the reason for the cancellation. + */ + public val reason: String? + get() = params.reason + + /** + * Metadata for this notification. + */ + public val meta: JsonObject? + get() = params.meta +} + +/** + * Parameters for a notifications/cancelled notification. + * + * @property requestId The ID of the request to cancel. + * This MUST correspond to the ID of a request previously issued in the same direction. + * @property reason An optional string describing the reason for the cancellation. + * This MAY be logged or presented to the user. + * @property meta Optional metadata for this notification. + */ +@Serializable +public data class CancelledNotificationParams( + val requestId: RequestId, + val reason: String? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : NotificationParams + +// ============================================================================ +// Initialized Notification +// ============================================================================ + +/** + * This notification is sent from the client to the server after initialization has finished. + * + * The client sends this after receiving the [InitializeResult] to signal that it is ready + * to begin normal operations. + * + * @property params Optional notification parameters containing metadata. + */ +@Serializable +public data class InitializedNotification(override val params: BaseNotificationParams? = null) : ClientNotification { + @EncodeDefault + override val method: Method = Method.Defined.NotificationsInitialized +} + +// ============================================================================ +// Logging Message Notification +// ============================================================================ + +/** + * A notification of a log message passed from server to client. + * + * If no [SetLevelRequest] has been sent from the client, the server MAY decide + * which messages to send automatically. + * + * @property params The log message parameters including level and data. + */ +@Serializable +public data class LoggingMessageNotification(override val params: LoggingMessageNotificationParams) : + ServerNotification { + @EncodeDefault + override val method: Method = Method.Defined.NotificationsMessage +} + +/** + * Parameters for a notifications/message notification. + * + * @property level The severity of this log message. + * @property data The data to be logged, such as a string message or an object. + * Any JSON serializable type is allowed here. + * @property logger An optional name of the logger issuing this message. + * @property meta Optional metadata for this notification. + */ +@Serializable +public data class LoggingMessageNotificationParams( + val level: LoggingLevel, + val data: JsonElement, + val logger: String? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : NotificationParams + +// ============================================================================ +// Progress Notification +// ============================================================================ + +/** + * An out-of-band notification used to inform the receiver of a progress update for a long-running request. + * + * This notification can be sent by either the client or server to provide progress updates + * for requests that include a progressToken in their _meta field. + * + * @property params The progress update parameters. + */ +@Serializable +public data class ProgressNotification(override val params: ProgressNotificationParams) : + ClientNotification, + ServerNotification { + @EncodeDefault + override val method: Method = Method.Defined.NotificationsProgress +} + +/** + * Parameters for a notifications/progress notification. + * + * @property progressToken The progress token which was given in the initial request, + * used to associate this notification with the request that is proceeding. + * @property progress The progress thus far. This should increase every time progress is made, + * even if the total is unknown. + * @property total Total number of items to process (or total progress required), if known. + * @property message An optional message describing the current progress. + * @property meta Optional metadata for this notification. + */ +@Serializable +public data class ProgressNotificationParams( + val progressToken: ProgressToken, + val progress: Double, + val total: Double? = null, + val message: String? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : NotificationParams + +// ============================================================================ +// Prompts List Changed Notification +// ============================================================================ + +/** + * An optional notification from the server to the client, informing it that the list of prompts it offers has changed. + * + * Servers may issue this without any previous subscription from the client. + * Sent only if the server's [ServerCapabilities.prompts] has `listChanged = true`. + * + * @property params Optional notification parameters containing metadata. + */ +@Serializable +public data class PromptListChangedNotification(override val params: BaseNotificationParams? = null) : + ServerNotification { + @EncodeDefault + override val method: Method = Method.Defined.NotificationsPromptsListChanged +} + +// ============================================================================ +// Resources List Changed Notification +// ============================================================================ + +/** + * An optional notification from the server to the client, + * informing it that the list of resources it can read from has changed. + * + * Servers may issue this without any previous subscription from the client. + * Sent only if the server's [ServerCapabilities.resources] has `listChanged = true`. + * + * @property params Optional notification parameters containing metadata. + */ +@Serializable +public data class ResourceListChangedNotification(override val params: BaseNotificationParams? = null) : + ServerNotification { + @EncodeDefault + override val method: Method = Method.Defined.NotificationsResourcesListChanged +} + +// ============================================================================ +// Resource Updated Notification +// ============================================================================ + +/** + * A notification from the server to the client, informing it that a resource has changed and may need to be read again. + * + * This should only be sent if the client previously sent a resources/subscribe request + * and the server's [ServerCapabilities.resources] has `subscribe = true`. + * + * @property params Parameters identifying which resource was updated. + */ +@Serializable +public data class ResourceUpdatedNotification(override val params: ResourceUpdatedNotificationParams) : + ServerNotification { + @EncodeDefault + override val method: Method = Method.Defined.NotificationsResourcesUpdated +} + +/** + * Parameters for a notifications/resources/updated notification. + * + * @property uri The URI of the resource that has been updated. + * This might be a sub-resource of the one that the client actually subscribed to. + * @property meta Optional metadata for this notification. + */ +@Serializable +public data class ResourceUpdatedNotificationParams( + val uri: String, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : NotificationParams + +// ============================================================================ +// Roots List Changed Notification +// ============================================================================ + +/** + * A notification from the client to the server, informing it that the list of roots has changed. + * + * This notification should be sent whenever the client adds, removes, or modifies any root. + * The server should then request an updated list of roots using the ListRootsRequest. + * Sent only if the client's [ClientCapabilities.roots] has `listChanged = true`. + * + * @property params Optional notification parameters containing metadata. + */ +@Serializable +public data class RootsListChangedNotification(override val params: BaseNotificationParams? = null) : + ClientNotification { + @EncodeDefault + override val method: Method = Method.Defined.NotificationsRootsListChanged +} + +// ============================================================================ +// Elicitation Complete Notification +// ============================================================================ + +/** + * An optional notification from the server to the client, + * informing it of a completion of an out-of-band elicitation request. + * + * @property params Parameters identifying which elicitation completed. + */ +@Serializable +public data class ElicitationCompleteNotification(override val params: ElicitationCompleteNotificationParams) : + ServerNotification { + @EncodeDefault + override val method: Method = Method.Defined.NotificationsElicitationComplete +} + +/** + * Parameters for a notifications/elicitation/complete notification. + * + * @property elicitationId The ID of the elicitation that completed. + * @property meta Optional metadata for this notification. + */ +@Serializable +public data class ElicitationCompleteNotificationParams( + val elicitationId: String, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : NotificationParams + +// ============================================================================ +// Tools List Changed Notification +// ============================================================================ + +/** + * An optional notification from the server to the client, informing it that the list of tools it offers has changed. + * + * Servers may issue this without any previous subscription from the client. + * Sent only if the server's [ServerCapabilities.tools] has `listChanged = true`. + * + * @property params Optional notification parameters containing metadata. + */ +@Serializable +public data class ToolListChangedNotification(override val params: BaseNotificationParams? = null) : + ServerNotification { + @EncodeDefault + override val method: Method = Method.Defined.NotificationsToolsListChanged +} + +// ============================================================================ +// Task Status Notification +// ============================================================================ + +/** + * An optional notification from the receiver to the requestor, informing them that a task’s status has changed. + * Receivers are not required to send these notifications. + * + * This notification can be sent by either side (both [ClientNotification] and [ServerNotification]). + * + * @property params The task status notification parameters containing the current task state. + */ +@Serializable +public data class TaskStatusNotification(override val params: TaskStatusNotificationParams? = null) : + ClientNotification, + ServerNotification { + @EncodeDefault + override val method: Method = Method.Defined.NotificationsTasksStatus +} + +/** + * Parameters for a notifications/tasks/status notification. + * + * @property taskId The task identifier. + * @property status Current task state. + * @property statusMessage Optional human-readable message describing the current task state. + * @property createdAt ISO 8601 timestamp when the task was created. + * @property lastUpdatedAt ISO 8601 timestamp when the task was last updated. + * @property ttl Actual retention duration from creation in milliseconds, null for unlimited. + * @property pollInterval Suggested polling interval in milliseconds. + * @property meta Optional metadata for this notification. + */ +@Serializable +public data class TaskStatusNotificationParams( + override val taskId: String, + override val status: TaskStatus, + override val statusMessage: String? = null, + override val createdAt: String, + override val lastUpdatedAt: String, + override val ttl: Long?, + override val pollInterval: Long? = null, + @SerialName("_meta") override val meta: JsonObject? = null, +) : NotificationParams, + TaskFields diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/pingRequest.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/pingRequest.dsl.kt new file mode 100644 index 0000000..1898636 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/pingRequest.dsl.kt @@ -0,0 +1,59 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * Creates a [PingRequest] using a type-safe DSL builder. + * + * ## Optional + * - [meta][PingRequestBuilder.meta] - Metadata for the request + * + * Example with no parameters: + * ```kotlin + * val request = buildPingRequest { } + * ``` + * + * Example with metadata: + * ```kotlin + * val request = buildPingRequest { + * meta { + * put("timestamp", JsonPrimitive(System.currentTimeMillis())) + * } + * } + * ``` + * + * @param block Configuration lambda for setting up the ping request + * @return A configured [PingRequest] instance + * @see PingRequestBuilder + * @see PingRequest + */ +@OptIn(ExperimentalContracts::class) +@ExperimentalMcpApi +public inline fun buildPingRequest(block: PingRequestBuilder.() -> Unit): PingRequest { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + return PingRequestBuilder().apply(block).build() +} + +/** + * DSL builder for constructing [PingRequest] instances. + * + * This builder creates ping requests to check connection status with the server. + * All fields are optional. + * + * ## Optional + * - [meta] - Metadata for the request + * + * @see buildPingRequest + * @see PingRequest + */ +@McpDsl +public class PingRequestBuilder @PublishedApi internal constructor() : RequestBuilder() { + @PublishedApi + override fun build(): PingRequest { + val params = meta?.let { BaseRequestParams(meta = it) } + return PingRequest(params) + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/prompts.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/prompts.dsl.kt new file mode 100644 index 0000000..b5aaaa2 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/prompts.dsl.kt @@ -0,0 +1,145 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * Creates a [GetPromptRequest] using a type-safe DSL builder. + * + * ## Required + * - [name][GetPromptRequestBuilder.name] - The name of the prompt to retrieve + * + * ## Optional + * - [arguments][GetPromptRequestBuilder.arguments] - Arguments to pass to the prompt + * - [meta][GetPromptRequestBuilder.meta] - Metadata for the request + * + * Example without arguments: + * ```kotlin + * val request = buildGetPromptRequest { + * name = "greeting" + * } + * ``` + * + * Example with arguments: + * ```kotlin + * val request = buildGetPromptRequest { + * name = "userProfile" + * arguments = mapOf( + * "userId" to "123", + * "includeDetails" to "true" + * ) + * } + * ``` + * + * @param block Configuration lambda for setting up the get prompt request + * @return A configured [GetPromptRequest] instance + * @see GetPromptRequestBuilder + * @see GetPromptRequest + */ +@OptIn(ExperimentalContracts::class) +@ExperimentalMcpApi +public inline fun buildGetPromptRequest(block: GetPromptRequestBuilder.() -> Unit): GetPromptRequest { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + return GetPromptRequestBuilder().apply(block).build() +} + +/** + * DSL builder for constructing [GetPromptRequest] instances. + * + * This builder retrieves a specific prompt by name, optionally with arguments. + * + * ## Required + * - [name] - The name of the prompt to retrieve + * + * ## Optional + * - [arguments] - Arguments to pass to the prompt + * - [meta] - Metadata for the request + * + * @see buildGetPromptRequest + * @see GetPromptRequest + */ +@McpDsl +public class GetPromptRequestBuilder @PublishedApi internal constructor() : RequestBuilder() { + /** + * The name of the prompt to retrieve. This is a required field. + * + * Example: `name = "greeting"` + */ + public var name: String? = null + + /** + * Optional arguments to pass to the prompt. + * + * Example: + * ```kotlin + * arguments = mapOf("userId" to "123", "lang" to "en") + * ``` + */ + public var arguments: Map? = null + + @PublishedApi + override fun build(): GetPromptRequest { + val name = requireNotNull(name) { + "Missing required field 'name'. Example: name = \"promptName\"" + } + + val params = GetPromptRequestParams(name = name, arguments = arguments, meta = meta) + return GetPromptRequest(params) + } +} + +/** + * Creates a [ListPromptsRequest] using a type-safe DSL builder. + * + * ## Optional + * - [cursor][ListPromptsRequestBuilder.cursor] - Pagination cursor for fetching next page + * - [meta][ListPromptsRequestBuilder.meta] - Metadata for the request + * + * Example without pagination: + * ```kotlin + * val request = buildListPromptsRequest { } + * ``` + * + * Example with pagination: + * ```kotlin + * val request = buildListPromptsRequest { + * cursor = "eyJwYWdlIjogMn0=" + * } + * ``` + * + * @param block Configuration lambda for setting up the list prompts request + * @return A configured [ListPromptsRequest] instance + * @see ListPromptsRequestBuilder + * @see ListPromptsRequest + */ +@OptIn(ExperimentalContracts::class) +@ExperimentalMcpApi +public inline fun buildListPromptsRequest(block: ListPromptsRequestBuilder.() -> Unit): ListPromptsRequest { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + return ListPromptsRequestBuilder().apply(block).build() +} + +/** + * DSL builder for constructing [ListPromptsRequest] instances. + * + * This builder retrieves a list of available prompts, with optional pagination support. + * All fields are optional. + * + * ## Optional + * - [cursor] - Pagination cursor (inherited from [PaginatedRequestBuilder]) + * - [meta] - Metadata for the request (inherited from [RequestBuilder]) + * + * @see buildListPromptsRequest + * @see ListPromptsRequest + * @see PaginatedRequestBuilder + */ +@McpDsl +public class ListPromptsRequestBuilder @PublishedApi internal constructor() : PaginatedRequestBuilder() { + @PublishedApi + override fun build(): ListPromptsRequest { + val params = paginatedRequestParams(cursor = cursor, meta = meta) + return ListPromptsRequest(params) + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/prompts.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/prompts.kt new file mode 100644 index 0000000..5d7054b --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/prompts.kt @@ -0,0 +1,212 @@ +@file:OptIn(ExperimentalSerializationApi::class) + +package io.modelcontextprotocol.kotlin.sdk.types + +import kotlinx.serialization.EncodeDefault +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject + +/** + * Describes an argument that a prompt can accept. + * + * Arguments allow prompts to be templated with user-provided values. + * + * @property name The programmatic identifier for this argument. + * Intended for logical use and API identification. If [title] is not provided, + * this should be used as a fallback display name. + * @property description A human-readable description of the argument, explaining what it's for + * and what values are expected. + * @property required Whether this argument must be provided when using the prompt. + * If true, the client must supply a value for this argument. + * @property title Optional human-readable display name for this argument. + * Intended for UI and end-user contexts, optimized to be easily understood + * even by those unfamiliar with domain-specific terminology. + * If not provided, [name] should be used for display purposes. + */ +@Serializable +public data class PromptArgument( + val name: String, + val description: String? = null, + val required: Boolean? = null, + val title: String? = null, +) + +/** + * A prompt or prompt template that the server offers. + * + * Prompts can be static messages or templates that accept arguments for dynamic content generation. + * + * @property name The programmatic identifier for this prompt. + * Intended for logical use and API identification. If [title] is not provided, + * this should be used as a fallback display name. + * @property description An optional description of what this prompt provides and when to use it. + * @property arguments Optional list of arguments to use for templating the prompt. + * If present, the prompt is a template that requires these arguments to be filled in. + * @property title Optional human-readable display name for this prompt. + * Intended for UI and end-user contexts, optimized to be easily understood + * even by those unfamiliar with domain-specific terminology. + * If not provided, [name] should be used for display purposes. + * @property icons Optional set of sized icons that clients can display in their user interface. + * Clients MUST support at least PNG and JPEG formats. + * Clients SHOULD also support SVG and WebP formats. + * @property meta Optional metadata for this prompt. + */ +@Serializable +public data class Prompt( + val name: String, + val description: String? = null, + val arguments: List? = null, + val title: String? = null, + val icons: List? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : WithMeta + +/** + * Describes a message returned as part of a prompt. + * + * This is similar to SamplingMessage, but also supports the embedding of resources from the MCP server. + * + * @property role The role of the message sender (e.g., user, assistant, or system). + * @property content The content of the message. Can include text, images, resources, and other content types. + */ +@Serializable +public data class PromptMessage(val role: Role, val content: ContentBlock) + +/** + * Identifies a prompt by reference. + * + * Used in completion requests and other contexts where a prompt needs to be referenced + * without including its full definition. + * + * @property name The programmatic identifier of the prompt being referenced. + * Intended for logical use and API identification. If [title] is not provided, + * this should be used as a fallback display name. + * @property title Optional human-readable display name for the referenced prompt. + * Intended for UI and end-user contexts, optimized to be easily understood + * even by those unfamiliar with domain-specific terminology. + * If not provided, [name] should be used for display purposes. + */ +@Serializable +public data class PromptReference(val name: String, val title: String? = null) : Reference { + @EncodeDefault + public override val type: ReferenceType = ReferenceType.Prompt +} + +// ============================================================================ +// prompts/get +// ============================================================================ + +/** + * Used by the client to get a prompt provided by the server. + * + * Prompts can be static text or templates that accept arguments. When arguments + * are provided, the server will substitute them into the prompt template before + * returning the result. + * + * @property params The parameters specifying which prompt to retrieve and any template arguments. + */ +@Serializable +public data class GetPromptRequest(override val params: GetPromptRequestParams) : ClientRequest { + @EncodeDefault + override val method: Method = Method.Defined.PromptsGet + + /** + * The name of the prompt or prompt template to retrieve. + */ + public val name: String + get() = params.name + + /** + * Arguments to use for templating the prompt. + * Keys are argument names, values are the argument values to substitute. + */ + public val arguments: Map? + get() = params.arguments + + /** + * Metadata for this request. May include a progressToken for out-of-band progress notifications. + */ + public val meta: RequestMeta? + get() = params.meta +} + +/** + * Parameters for a prompts/get request. + * + * @property name The name of the prompt or prompt template to retrieve. + * @property arguments Optional arguments to use for templating the prompt. + * Keys are argument names, values are the argument values to substitute. + * @property meta Optional metadata for this request. May include a progressToken for + * out-of-band progress notifications. + */ +@Serializable +public data class GetPromptRequestParams( + val name: String, + val arguments: Map? = null, + @SerialName("_meta") + override val meta: RequestMeta? = null, +) : RequestParams + +/** + * The server's response to a [GetPromptRequest] from the client. + * + * Contains the prompt's messages and optional description. If the prompt was a template, + * the messages will have arguments already substituted. + * + * @property messages The list of messages that make up this prompt. + * Each message has a role (e.g., "user", "assistant") and content. + * @property description An optional description for the prompt, explaining its purpose or usage. + * @property meta Optional metadata for this response. + */ +@Serializable +public data class GetPromptResult( + val messages: List, + val description: String? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : ServerResult + +// ============================================================================ +// prompts/list +// ============================================================================ + +/** + * Sent from the client to request a list of prompts and prompt templates the server has. + * + * This request supports pagination through the cursor parameter. If the server has many + * prompts, it may return a subset along with a cursor to fetch the next page. + * + * @property params Optional pagination parameters to control which page of results to return. + */ +@Serializable +public data class ListPromptsRequest(override val params: PaginatedRequestParams? = null) : + ClientRequest, + PaginatedRequest { + @EncodeDefault + override val method: Method = Method.Defined.PromptsList +} + +/** + * The server's response to a [ListPromptsRequest] from the client. + * + * Returns the available prompts and prompt templates, along with pagination information + * if there are more results available. + * + * @property prompts The list of available prompts. Each prompt includes its name, optional description, + * and information about any arguments it accepts. + * @property nextCursor An opaque token representing the pagination position after the last returned result. + * If present, there may be more results available. The client can pass this token + * in a subsequent request to fetch the next page. + * @property meta Optional metadata for this response. + */ +@Serializable +public data class ListPromptsResult( + val prompts: List, + override val nextCursor: String? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : ServerResult, + PaginatedResult diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.dsl.kt new file mode 100644 index 0000000..3e2a150 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.dsl.kt @@ -0,0 +1,208 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import kotlinx.serialization.json.JsonArrayBuilder +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonObjectBuilder +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject + +/** + * Base DSL builder for constructing MCP request instances. + * + * This abstract class provides common functionality for all request builders, + * including optional metadata support. + * + * All concrete request builder classes extend this base to inherit [meta] functionality. + * + * @see RequestMetaBuilder + * @see PaginatedRequestBuilder + */ +@McpDsl +public abstract class RequestBuilder { + protected var meta: RequestMeta? = null + + /** + * Sets request metadata using a DSL builder. + * + * Metadata can include progress tokens and custom fields for tracking requests. + * + * Example: + * ```kotlin + * initializeRequest { + * protocolVersion = "2024-11-05" + * meta { + * progressToken("progress-123") + * put("customField", "value") + * } + * // ... other configuration + * } + * ``` + * + * @param builderAction Lambda for building request metadata + * @see RequestMetaBuilder + */ + public fun meta(builderAction: RequestMetaBuilder.() -> Unit) { + meta = RequestMetaBuilder().apply(builderAction).build() + } + + internal abstract fun build(): Request +} + +/** + * DSL builder for constructing [RequestMeta] instances. + * + * This builder creates metadata for MCP requests, supporting progress tokens + * and custom key-value pairs. + * + * Example: + * ```kotlin + * meta { + * progressToken("progress-123") + * put("requestId", "req-456") + * put("priority", 1) + * putJsonObject("context") { + * put("source", "api") + * } + * } + * ``` + * + * @see RequestMeta + * @see RequestBuilder.meta + */ +@McpDsl +public class RequestMetaBuilder internal constructor() { + private val content: MutableMap = linkedMapOf() + + /** + * Sets the progress token as a string. + * + * Progress tokens are used for out-of-band progress notifications. + * + * Example: `progressToken("progress-123")` + */ + public fun progressToken(value: String) { + content["progressToken"] = JsonPrimitive(value) + } + + /** + * Sets the progress token as a `Long`. + * + * Example: `progressToken(123L)` + */ + public fun progressToken(value: Long) { + content["progressToken"] = JsonPrimitive(value) + } + + /** + * Sets the progress token as an Int. + * + * Example: `progressToken(123)` + */ + public fun progressToken(value: Int) { + content["progressToken"] = JsonPrimitive(value) + } + + /** + * Adds a custom metadata field with a JsonElement value. + * + * @param key The metadata field name + * @param value The JsonElement value + * @return The previous value associated with the key, or null + */ + public fun put(key: String, value: JsonElement): JsonElement? = content.put(key, value) + + /** + * Adds a custom metadata field with a String value. + * + * Example: `put("requestId", "req-456")` + */ + public fun put(key: String, value: String): JsonElement? = put(key, JsonPrimitive(value)) + + /** + * Adds a custom metadata field with a Number value. + * + * Example: `put("priority", 1)` + */ + public fun put(key: String, value: Number): JsonElement? = put(key, JsonPrimitive(value)) + + /** + * Adds a custom metadata field with a Boolean value. + * + * Example: `put("urgent", true)` + */ + public fun put(key: String, value: Boolean): JsonElement? = put(key, JsonPrimitive(value)) + + /** + * Adds a custom metadata field with a null value. + * + * Example: `put("optionalField", null)` + */ + @Suppress("UNUSED_PARAMETER") + public fun put(key: String, value: Nothing?): JsonElement? = put(key, JsonNull) + + /** + * Adds a custom metadata field with a JsonObject value using a DSL builder. + * + * Example: + * ```kotlin + * putJsonObject("context") { + * put("source", "api") + * put("version", 2) + * } + * ``` + * + * @param key The metadata field name + * @param builderAction Lambda for building the JsonObject + */ + public fun putJsonObject(key: String, builderAction: JsonObjectBuilder.() -> Unit): JsonElement? = + put(key, buildJsonObject(builderAction)) + + /** + * Adds a custom metadata field with a JsonArray value using a DSL builder. + * + * Example: + * ```kotlin + * putJsonArray("tags") { + * add("important") + * add("urgent") + * } + * ``` + * + * @param key The metadata field name + * @param builderAction Lambda for building the JsonArray + */ + public fun putJsonArray(key: String, builderAction: JsonArrayBuilder.() -> Unit): JsonElement? = + put(key, buildJsonArray(builderAction)) + + internal fun build(): RequestMeta = RequestMeta(JsonObject(content)) +} + +/** + * Base DSL builder for constructing paginated MCP request instances. + * + * This abstract class extends [RequestBuilder] and adds pagination support + * through an optional cursor field. + * + * Example: + * ```kotlin + * listPromptsRequest { + * cursor = "eyJwYWdlIjogMn0=" + * } + * ``` + * + * @see RequestBuilder + */ +@McpDsl +public abstract class PaginatedRequestBuilder : RequestBuilder() { + /** + * Optional pagination cursor for fetching the next page of results. + * + * The cursor value is typically obtained from the previous page's response. + * + * Example: `cursor = "eyJwYWdlIjogMn0="` + */ + public var cursor: String? = null +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.kt new file mode 100644 index 0000000..7d885fb --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.kt @@ -0,0 +1,272 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.long +import kotlinx.serialization.json.longOrNull +import kotlin.jvm.JvmInline + +internal object RequestMetaKeys { + const val PROTOCOL_VERSION: String = "io.modelcontextprotocol/protocolVersion" + const val CLIENT_INFO: String = "io.modelcontextprotocol/clientInfo" + const val CLIENT_CAPABILITIES: String = "io.modelcontextprotocol/clientCapabilities" + const val LOG_LEVEL: String = "io.modelcontextprotocol/logLevel" +} + +/** + * Metadata attached to a request's `_meta` field. + * + * @property json the raw JSON object containing the metadata + */ +@JvmInline +@Serializable +public value class RequestMeta(public val json: JsonObject) { + /** Optional progress token for tracking request progress. */ + public val progressToken: ProgressToken? + get() = json["progressToken"]?.let { element -> + when { + element is JsonPrimitive && element.isString -> ProgressToken(element.content) + element is JsonPrimitive && element.longOrNull != null -> ProgressToken(element.long) + else -> null + } + } + + /** + * The related task metadata, if this request is associated with a task. + * + * @see RelatedTaskMetadata + * @see RELATED_TASK_META_KEY + */ + public val relatedTask: RelatedTaskMetadata? + get() = json[RELATED_TASK_META_KEY]?.let { element -> + McpJson.decodeFromJsonElement(element) + } + + /** + * The MCP protocol version selected for this request, or `null` when absent. + * + * This field is required by the request-scoped lifecycle introduced in protocol version + * `2026-07-28`, but remains optional here so older requests retain their wire shape. + * + * @throws SerializationException if the field is present but is not a string + */ + @ExperimentalMcpApi + public val protocolVersion: String? + get() = json[RequestMetaKeys.PROTOCOL_VERSION]?.let { element -> + if (element !is JsonPrimitive || !element.isString) { + throw SerializationException("${RequestMetaKeys.PROTOCOL_VERSION} must be a JSON string") + } + element.content + } + + /** + * Information about the client making this request, or `null` when absent. + * + * Request-scoped clients should include this field unless specifically configured not to do + * so. The self-reported value is intended for display, logging, and debugging; servers should + * not use it to change behavior or rely on it for security decisions. + * + * @throws SerializationException if the field is present but is not a valid [Implementation] + */ + @ExperimentalMcpApi + public val clientInfo: Implementation? + get() = json[RequestMetaKeys.CLIENT_INFO]?.let { element -> + try { + McpJson.decodeFromJsonElement(element) + } catch (cause: SerializationException) { + throw SerializationException( + "${RequestMetaKeys.CLIENT_INFO} must be a valid client implementation", + cause, + ) + } + } + + /** + * Capabilities declared by the client for this request, or `null` when absent. + * + * Servers using the request-scoped lifecycle must not infer capabilities from an earlier + * request. An empty object means the client supports no optional capabilities. + * + * @throws SerializationException if the field is present but is not valid [ClientCapabilities] + */ + @ExperimentalMcpApi + public val clientCapabilities: ClientCapabilities? + get() = json[RequestMetaKeys.CLIENT_CAPABILITIES]?.let { element -> + try { + McpJson.decodeFromJsonElement(element) + } catch (cause: SerializationException) { + throw SerializationException( + "${RequestMetaKeys.CLIENT_CAPABILITIES} must be a valid client capabilities object", + cause, + ) + } + } + + /** + * Minimum severity of request-associated log notifications, or `null` when absent. + * + * In the request-scoped lifecycle, absence means that the server must not emit log + * notifications for this request. + * + * @throws SerializationException if the field is present but is not a valid [LoggingLevel] + */ + @Deprecated("Per-request log levels are deprecated as of MCP protocol version 2026-07-28 (SEP-2577).") + @ExperimentalMcpApi + public val logLevel: LoggingLevel? + get() = json[RequestMetaKeys.LOG_LEVEL]?.let { element -> + try { + McpJson.decodeFromJsonElement(element) + } catch (cause: SerializationException) { + throw SerializationException( + "${RequestMetaKeys.LOG_LEVEL} must be a valid logging level", + cause, + ) + } + } + + /** + * Retrieves the value associated with the specified key from the JSON object. + * + * @param key the key whose corresponding value is to be returned + * @return the JsonElement associated with the specified key, or null if the key does not exist + */ + public operator fun get(key: String): JsonElement? = json[key] +} + +/** + * Base interface for parameters attached to a [Request]. + */ +@Serializable +public sealed interface RequestParams { + /** + * The `_meta` property/parameter is reserved by MCP + * to allow clients and servers to attach additional metadata to their interactions. + * + * @see MCP specification + */ + @SerialName("_meta") + public val meta: RequestMeta? +} + +/** + * Default [RequestParams] implementation carrying only optional metadata. + */ +@Serializable +public data class BaseRequestParams(@SerialName("_meta") override val meta: RequestMeta? = null) : RequestParams + +/** + * Represents a request in the protocol. + */ +@OptIn(ExperimentalSerializationApi::class) +@Serializable(with = RequestPolymorphicSerializer::class) +public sealed interface Request { + /** The request method identifier. */ + public val method: Method + + /** Optional request parameters. */ + public val params: RequestParams? +} + +/** + * A custom request with a specified method. + */ +@Serializable +public open class CustomRequest(override val method: Method, override val params: BaseRequestParams?) : Request + +/** + * Represents a request sent by the client. + */ +@Serializable +public sealed interface ClientRequest : Request + +/** + * Represents a request sent by the server. + */ +@Serializable +public sealed interface ServerRequest : Request + +/** + * Represents a request supporting pagination. + */ +@Serializable +public sealed interface PaginatedRequest : Request { + public override val params: PaginatedRequestParams? + + /** + * An opaque token representing the current pagination position. + */ + public val cursor: String? + get() = params?.cursor + + /** + * Metadata for this request. May include a progressToken for out-of-band progress notifications. + */ + public val meta: RequestMeta? + get() = params?.meta +} + +/** + * Represents the result of a request, including additional metadata. + */ +@Serializable(with = RequestResultPolymorphicSerializer::class) +public sealed interface RequestResult : WithMeta + +/** + * Represents a result returned by the server in response to a [ClientRequest]. + */ +@Serializable(with = ClientResultPolymorphicSerializer::class) +public sealed interface ClientResult : RequestResult + +/** + * Represents a result returned by the client in response to a [ServerRequest]. + */ +@Serializable(with = ServerResultPolymorphicSerializer::class) +public sealed interface ServerResult : RequestResult + +/** + * An empty result for a request containing optional metadata. + * + * @property meta Additional metadata for the response. Defaults to an empty JSON object. + */ +@Serializable +public data class EmptyResult(@SerialName("_meta") override val meta: JsonObject? = null) : + ClientResult, + ServerResult + +/** + * Represents a paginated result. + */ +@Serializable +public sealed interface PaginatedResult : RequestResult { + /** Opaque token for retrieving the next page of results, or `null` if no more results. */ + public val nextCursor: String? +} + +/** + * Common parameters for paginated requests. + * + * @property cursor An opaque token representing the current pagination position. + * If provided, the server should return results starting after this cursor. + * @property meta Optional metadata for this request. May include a progressToken for + * out-of-band progress notifications. + */ +@Serializable +public data class PaginatedRequestParams( + val cursor: String? = null, + @SerialName("_meta") + override val meta: RequestMeta? = null, +) : RequestParams + +internal fun paginatedRequestParams(cursor: String?, meta: RequestMeta?): PaginatedRequestParams? = + if (cursor == null && meta == null) { + null + } else { + PaginatedRequestParams(cursor = cursor, meta = meta) + } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/resources.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/resources.dsl.kt new file mode 100644 index 0000000..ff2ddf5 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/resources.dsl.kt @@ -0,0 +1,302 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * Creates a [ListResourcesRequest] using a type-safe DSL builder. + * + * ## Optional + * - [cursor][ListResourcesRequestBuilder.cursor] - Pagination cursor for fetching next page + * - [meta][ListResourcesRequestBuilder.meta] - Metadata for the request + * + * Example without pagination: + * ```kotlin + * val request = buildListResourcesRequest { } + * ``` + * + * Example with pagination: + * ```kotlin + * val request = buildListResourcesRequest { + * cursor = "eyJwYWdlIjogMn0=" + * } + * ``` + * + * @param block Configuration lambda for setting up the list resources request + * @return A configured [ListResourcesRequest] instance + * @see ListResourcesRequestBuilder + * @see ListResourcesRequest + */ +@OptIn(ExperimentalContracts::class) +@ExperimentalMcpApi +public inline fun buildListResourcesRequest(block: ListResourcesRequestBuilder.() -> Unit): ListResourcesRequest { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + return ListResourcesRequestBuilder().apply(block).build() +} + +/** + * DSL builder for constructing [ListResourcesRequest] instances. + * + * This builder retrieves a list of available resources, with optional pagination support. + * All fields are optional. + * + * ## Optional + * - [cursor] - Pagination cursor (inherited from [PaginatedRequestBuilder]) + * - [meta] - Metadata for the request (inherited from [RequestBuilder]) + * + * @see buildListResourcesRequest + * @see ListResourcesRequest + * @see PaginatedRequestBuilder + */ +@McpDsl +public class ListResourcesRequestBuilder @PublishedApi internal constructor() : PaginatedRequestBuilder() { + @PublishedApi + override fun build(): ListResourcesRequest { + val params = paginatedRequestParams(cursor = cursor, meta = meta) + return ListResourcesRequest(params) + } +} + +/** + * Creates a [ReadResourceRequest] using a type-safe DSL builder. + * + * ## Required + * - [uri][ReadResourceRequestBuilder.uri] - The URI of the resource to read + * + * ## Optional + * - [meta][ReadResourceRequestBuilder.meta] - Metadata for the request + * + * Example: + * ```kotlin + * val request = buildReadResourceRequest { + * uri = "file:///path/to/resource.txt" + * } + * ``` + * + * @param block Configuration lambda for setting up the read resource request + * @return A configured [ReadResourceRequest] instance + * @see ReadResourceRequestBuilder + * @see ReadResourceRequest + */ +@OptIn(ExperimentalContracts::class) +@ExperimentalMcpApi +public inline fun buildReadResourceRequest(block: ReadResourceRequestBuilder.() -> Unit): ReadResourceRequest { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + return ReadResourceRequestBuilder().apply(block).build() +} + +/** + * DSL builder for constructing [ReadResourceRequest] instances. + * + * This builder reads the contents of a specific resource by URI. + * + * ## Required + * - [uri] - The URI of the resource to read + * + * ## Optional + * - [meta] - Metadata for the request + * + * @see buildReadResourceRequest + * @see ReadResourceRequest + */ +@McpDsl +public class ReadResourceRequestBuilder @PublishedApi internal constructor() : RequestBuilder() { + /** + * The URI of the resource to read. This is a required field. + * + * Example: `uri = "file:///path/to/resource.txt"` + */ + public var uri: String? = null + + @PublishedApi + override fun build(): ReadResourceRequest { + val uri = requireNotNull(uri) { + "Missing required field 'uri'. Example: uri = \"file:///path/to/resource.txt\"" + } + + val params = ReadResourceRequestParams(uri = uri, meta = meta) + return ReadResourceRequest(params) + } +} + +/** + * Creates a [SubscribeRequest] using a type-safe DSL builder. + * + * ## Required + * - [uri][SubscribeRequestBuilder.uri] - The URI of the resource to subscribe to + * + * ## Optional + * - [meta][SubscribeRequestBuilder.meta] - Metadata for the request + * + * Example: + * ```kotlin + * val request = buildSubscribeRequest { + * uri = "file:///path/to/resource.txt" + * } + * ``` + * + * @param block Configuration lambda for setting up the subscribe request + * @return A configured [SubscribeRequest] instance + * @see SubscribeRequestBuilder + * @see SubscribeRequest + */ +@OptIn(ExperimentalContracts::class) +@ExperimentalMcpApi +public inline fun buildSubscribeRequest(block: SubscribeRequestBuilder.() -> Unit): SubscribeRequest { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + return SubscribeRequestBuilder().apply(block).build() +} + +/** + * DSL builder for constructing [SubscribeRequest] instances. + * + * This builder subscribes to updates for a specific resource by URI. + * + * ## Required + * - [uri] - The URI of the resource to subscribe to + * + * ## Optional + * - [meta] - Metadata for the request + * + * @see buildSubscribeRequest + * @see SubscribeRequest + */ +@McpDsl +public class SubscribeRequestBuilder @PublishedApi internal constructor() : RequestBuilder() { + /** + * The URI of the resource to subscribe to. This is a required field. + * + * Example: `uri = "file:///path/to/resource.txt"` + */ + public var uri: String? = null + + @PublishedApi + override fun build(): SubscribeRequest { + val uri = requireNotNull(uri) { + "Missing required field 'uri'. Example: uri = \"file:///path/to/resource.txt\"" + } + + val params = SubscribeRequestParams(uri = uri, meta = meta) + return SubscribeRequest(params) + } +} + +/** + * Creates an [UnsubscribeRequest] using a type-safe DSL builder. + * + * ## Required + * - [uri][UnsubscribeRequestBuilder.uri] - The URI of the resource to unsubscribe from + * + * ## Optional + * - [meta][UnsubscribeRequestBuilder.meta] - Metadata for the request + * + * Example: + * ```kotlin + * val request = buildUnsubscribeRequest { + * uri = "file:///path/to/resource.txt" + * } + * ``` + * + * @param block Configuration lambda for setting up the unsubscribe request + * @return A configured [UnsubscribeRequest] instance + * @see UnsubscribeRequestBuilder + * @see UnsubscribeRequest + */ +@OptIn(ExperimentalContracts::class) +@ExperimentalMcpApi +public inline fun buildUnsubscribeRequest(block: UnsubscribeRequestBuilder.() -> Unit): UnsubscribeRequest { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + return UnsubscribeRequestBuilder().apply(block).build() +} + +/** + * DSL builder for constructing [UnsubscribeRequest] instances. + * + * This builder unsubscribes from updates for a specific resource by URI. + * + * ## Required + * - [uri] - The URI of the resource to unsubscribe from + * + * ## Optional + * - [meta] - Metadata for the request + * + * @see buildUnsubscribeRequest + * @see UnsubscribeRequest + */ +@McpDsl +public class UnsubscribeRequestBuilder @PublishedApi internal constructor() : RequestBuilder() { + /** + * The URI of the resource to unsubscribe from. This is a required field. + * + * Example: `uri = "file:///path/to/resource.txt"` + */ + public var uri: String? = null + + @PublishedApi + override fun build(): UnsubscribeRequest { + val uri = requireNotNull(uri) { + "Missing required field 'uri'. Example: uri = \"file:///path/to/resource.txt\"" + } + + val params = UnsubscribeRequestParams(uri = uri, meta = meta) + return UnsubscribeRequest(params) + } +} + +/** + * Creates a [ListResourceTemplatesRequest] using a type-safe DSL builder. + * + * ## Optional + * - [cursor][ListResourceTemplatesRequestBuilder.cursor] - Pagination cursor for fetching next page + * - [meta][ListResourceTemplatesRequestBuilder.meta] - Metadata for the request + * + * Example without pagination: + * ```kotlin + * val request = buildListResourceTemplatesRequest { } + * ``` + * + * Example with pagination: + * ```kotlin + * val request = buildListResourceTemplatesRequest { + * cursor = "eyJwYWdlIjogMn0=" + * } + * ``` + * + * @param block Configuration lambda for setting up the list resource templates request + * @return A configured [ListResourceTemplatesRequest] instance + * @see ListResourceTemplatesRequestBuilder + * @see ListResourceTemplatesRequest + */ +@OptIn(ExperimentalContracts::class) +@ExperimentalMcpApi +public inline fun buildListResourceTemplatesRequest( + block: ListResourceTemplatesRequestBuilder.() -> Unit, +): ListResourceTemplatesRequest { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + return ListResourceTemplatesRequestBuilder().apply(block).build() +} + +/** + * DSL builder for constructing [ListResourceTemplatesRequest] instances. + * + * This builder retrieves a list of available resource templates, with optional pagination support. + * All fields are optional. + * + * ## Optional + * - [cursor] - Pagination cursor (inherited from [PaginatedRequestBuilder]) + * - [meta] - Metadata for the request (inherited from [RequestBuilder]) + * + * @see buildListResourceTemplatesRequest + * @see ListResourceTemplatesRequest + * @see PaginatedRequestBuilder + */ +@McpDsl +public class ListResourceTemplatesRequestBuilder @PublishedApi internal constructor() : PaginatedRequestBuilder() { + @PublishedApi + override fun build(): ListResourceTemplatesRequest { + val params = paginatedRequestParams(cursor = cursor, meta = meta) + return ListResourceTemplatesRequest(params) + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/resources.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/resources.kt new file mode 100644 index 0000000..1afa0e6 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/resources.kt @@ -0,0 +1,423 @@ +@file:OptIn(ExperimentalSerializationApi::class) + +package io.modelcontextprotocol.kotlin.sdk.types + +import kotlinx.serialization.EncodeDefault +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject + +/** + * Common interface for resource-like types that share URI, name, and description fields. + */ +@Serializable +public sealed interface ResourceLike : WithMeta + +/** + * A known resource that the server is capable of reading. + * + * Resources represent data sources such as files, database entries, API responses, + * or other structured data that can be read by clients. + * + * @property uri The URI of this resource. Can use any protocol scheme (`file://`, `http://`, etc.). + * @property name The programmatic identifier for this resource. + * Intended for logical use and API identification. If [title] is not provided, + * this should be used as a fallback display name. + * @property description A description of what this resource represents. + * Clients can use this to improve the LLM's understanding of available resources. + * It can be thought of like a "hint" to the model. + * @property mimeType The MIME type of this resource, if known (e.g., "text/plain", "application/json", "image/png"). + * @property size The size of the raw resource content in bytes + * (i.e., before base64 encoding or any tokenization), if known. + * Hosts can use this to display file sizes and estimate context window usage. + * @property title Optional human-readable display name for this resource. + * Intended for UI and end-user contexts, optimized to be easily understood + * even by those unfamiliar with domain-specific terminology. + * If not provided, [name] should be used for display purposes. + * @property annotations Optional annotations for the client. Provides additional metadata and hints + * about how to use or display this resource. + * @property icons Optional set of sized icons that clients can display in their user interface. + * Clients MUST support at least PNG and JPEG formats. + * Clients SHOULD also support SVG and WebP formats. + * @property meta Optional metadata for this resource. + */ +@Serializable +public data class Resource( + val uri: String, + val name: String, + val description: String? = null, + val mimeType: String? = null, + val size: Long? = null, + val title: String? = null, + val annotations: Annotations? = null, + val icons: List? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : ResourceLike + +/** + * A template description for resources available on the server. + * + * Resource templates allow servers to expose parameterized resources that clients can + * instantiate with specific values. Templates use RFC 6570 URI template syntax, + * where parameters are indicated with curly braces (e.g., `file:///{directory}/{filename}`). + * + * @property uriTemplate A URI template (according to RFC 6570) that can be used to construct resource URIs. + * Parameters are indicated with curly braces, e.g., `file:///{path}` or `db://users/{userId}`. + * @property name The programmatic identifier for this template. + * Intended for logical use and API identification. If [title] is not provided, + * this should be used as a fallback display name. + * @property description A description of what this template is for. + * Clients can use this to improve the LLM's understanding of available resources. + * It can be thought of like a "hint" to the model. + * @property mimeType The MIME type for all resources that match this template. + * This should only be included if all resources matching this template have the same type. + * For example, a file template might not have a MIME type since files can be of any type, + * but a database record template might always return JSON. + * @property title Optional human-readable display name for this template. + * Intended for UI and end-user contexts, optimized to be easily understood + * even by those unfamiliar with domain-specific terminology. + * If not provided, [name] should be used for display purposes. + * @property annotations Optional annotations for the client. Provides additional metadata and hints + * about how to use or display resources created from this template. + * @property icons Optional set of sized icons that clients can display in their user interface. + * Clients MUST support at least PNG and JPEG formats. + * Clients SHOULD also support SVG and WebP formats. + * @property meta Optional metadata for this template. + */ +@Serializable +public data class ResourceTemplate( + val uriTemplate: String, + val name: String, + val description: String? = null, + val mimeType: String? = null, + val title: String? = null, + val annotations: Annotations? = null, + val icons: List? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : WithMeta + +/** + * A reference to a resource or resource template definition. + * + * Used in completion requests and other contexts where a resource needs to be referenced + * without including its full definition. The URI can be either a specific resource URI + * or a URI template pattern. + * + * @property uri The URI or URI template of the resource. + * Can be a specific resource URI (e.g., `file:///home/user/doc.txt`) + * or a URI template with parameters (e.g., `file:///{path}`). + */ +@Serializable +public data class ResourceTemplateReference(val uri: String) : Reference { + @EncodeDefault + public override val type: ReferenceType = ReferenceType.ResourceTemplate +} + +/** + * The contents of a specific resource or sub-resource. Carries the optional `_meta` field via [WithMeta]. + */ +@Serializable(with = ResourceContentsPolymorphicSerializer::class) +public sealed interface ResourceContents : WithMeta { + /** The URI of this resource. */ + public val uri: String + + /** The MIME type of this resource, if known. */ + public val mimeType: String? +} + +/** + * Represents the text contents of a resource. + * + * @property text The text of the item. + * This must only be set if the item can actually be represented as text (not binary data). + * @property uri The URI of this resource. + * @property mimeType The MIME type of this resource, if known. + * @property meta Optional metadata for these contents. + */ +@Serializable +public data class TextResourceContents( + val text: String, + override val uri: String, + override val mimeType: String? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : ResourceContents + +/** + * The contents of a specific resource or sub-resource. + * + * @property blob A base64-encoded string representing the binary data of the item. + * @property uri The URI of this resource. + * @property mimeType The MIME type of this resource, if known. + * @property meta Optional metadata for these contents. + */ +@Serializable +public data class BlobResourceContents( + val blob: String, + override val uri: String, + override val mimeType: String? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : ResourceContents + +/** + * Represents resource contents with unknown or unspecified data. + * + * @property uri The URI of this resource. + * @property mimeType The MIME type of this resource, if known. + * @property meta Optional metadata for these contents. + */ +@Serializable +public data class UnknownResourceContents( + override val uri: String, + override val mimeType: String? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : ResourceContents + +// ============================================================================ +// resources/list +// ============================================================================ + +/** + * Sent from the client to request a list of resources the server has. + * + * Resources are data sources that the server can provide access to, such as files, + * database entries, API responses, or other structured data. + * + * @property params Optional pagination parameters to control which page of results to return. + */ +@Serializable +public data class ListResourcesRequest(override val params: PaginatedRequestParams? = null) : + ClientRequest, + PaginatedRequest { + @EncodeDefault + override val method: Method = Method.Defined.ResourcesList +} + +/** + * The server's response to a [ListResourcesRequest] from the client. + * + * Returns the available resources along with pagination information if there are more results. + * + * @property resources The list of available resources. Each resource includes its URI, name, + * optional description, and MIME type information. + * @property nextCursor An opaque token representing the pagination position after the last returned result. + * If present, there may be more results available. The client can pass this token + * in a subsequent request to fetch the next page. + * @property meta Optional metadata for this response. + */ +@Serializable +public data class ListResourcesResult( + val resources: List, + override val nextCursor: String? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : ServerResult, + PaginatedResult + +// ============================================================================ +// resources/read +// ============================================================================ + +/** + * Sent from the client to the server to read a specific resource URI. + * + * The server will return the resource's contents, which can be either text or binary data. + * + * @property params The parameters specifying which resource URI to read. + */ +@Serializable +public data class ReadResourceRequest(override val params: ReadResourceRequestParams) : ClientRequest { + @EncodeDefault + override val method: Method = Method.Defined.ResourcesRead + + /** + * The URI of the resource to read + */ + public val uri: String + get() = params.uri + + /** + * Metadata for this request. May include a progressToken for out-of-band progress notifications. + */ + public val meta: RequestMeta? + get() = params.meta +} + +/** + * Parameters for a resources/read request. + * + * @property uri The URI of the resource to read. The URI can use any protocol; + * it is up to the server how to interpret it. Common schemes include + * file://, http://, https://, or custom application-specific schemes. + * @property meta Optional metadata for this request. May include a progressToken for + * out-of-band progress notifications. + */ +@Serializable +public data class ReadResourceRequestParams( + val uri: String, + @SerialName("_meta") + override val meta: RequestMeta? = null, +) : RequestParams + +/** + * The server's response to a [ReadResourceRequest] from the client. + * + * Contains the resource contents, which can be a mix of text and binary data. + * A single resource can return multiple content blocks if it contains multiple parts. + * + * @property contents The contents of the resource. Can include text content (with MIME type and text data) + * or binary/blob content (with MIME type and Base64-encoded data). + * Multiple content blocks can be returned for resources with multiple parts. + * @property meta Optional metadata for this response. + */ +@Serializable +public data class ReadResourceResult( + val contents: List, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : ServerResult + +// ============================================================================ +// resources/subscribe +// ============================================================================ + +/** + * Sent from the client to request resources/updated notifications from the server + * whenever a particular resource changes. + * + * After subscribing, the server will send [ResourceUpdatedNotification] messages + * whenever the subscribed resource is modified. This requires the server to support + * the `subscribe` capability in [ServerCapabilities.resources]. + * + * @property params The parameters specifying which resource URI to subscribe to. + */ +@Serializable +public data class SubscribeRequest(override val params: SubscribeRequestParams) : ClientRequest { + @EncodeDefault + override val method: Method = Method.Defined.ResourcesSubscribe + + /** + * The URI of the resource to subscribe to. + */ + public val uri: String + get() = params.uri + + /** + * Metadata for this request. May include a progressToken for out-of-band progress notifications. + */ + public val meta: RequestMeta? + get() = params.meta +} + +/** + * Parameters for a resources/subscribe request. + * + * @property uri The URI of the resource to subscribe to. The URI can use any protocol; + * it is up to the server how to interpret it. + * @property meta Optional metadata for this request. May include a progressToken for + * out-of-band progress notifications. + */ +@Serializable +public data class SubscribeRequestParams( + val uri: String, + @SerialName("_meta") + override val meta: RequestMeta? = null, +) : RequestParams + +// ============================================================================ +// resources/unsubscribe +// ============================================================================ + +/** + * Sent from the client to request cancellation of resources/updated notifications from the server. + * + * This should follow a previous [SubscribeRequest]. After unsubscribing, the server will + * stop sending [ResourceUpdatedNotification] messages for this resource. + * + * @property params The parameters specifying which resource URI to unsubscribe from. + */ +@Serializable +public data class UnsubscribeRequest(override val params: UnsubscribeRequestParams) : ClientRequest { + @EncodeDefault + override val method: Method = Method.Defined.ResourcesUnsubscribe + + /** + * The URI of the resource to unsubscribe from. + */ + public val uri: String + get() = params.uri + + /** + * Metadata for this request. May include a progressToken for out-of-band progress notifications. + */ + public val meta: RequestMeta? + get() = params.meta + + public constructor( + uri: String, + meta: RequestMeta? = null, + ) : this(UnsubscribeRequestParams(uri, meta)) +} + +/** + * Parameters for a resources/unsubscribe request. + * + * @property uri The URI of the resource to unsubscribe from. This should match + * a URI from a previous [SubscribeRequest]. + * @property meta Optional metadata for this request. May include a progressToken for + * out-of-band progress notifications. + */ +@Serializable +public data class UnsubscribeRequestParams( + val uri: String, + @SerialName("_meta") + override val meta: RequestMeta? = null, +) : RequestParams + +// ============================================================================ +// resources/templates/list +// ============================================================================ + +/** + * Sent from the client to request a list of resource templates the server has. + * + * Resource templates are parameterized resource URIs that can be instantiated with + * specific argument values. For example, a template like `file:///{path}` allows + * clients to construct URIs for any file path. + * + * @property params Optional pagination parameters to control which page of results to return. + */ +@Serializable +public data class ListResourceTemplatesRequest(override val params: PaginatedRequestParams? = null) : + ClientRequest, + PaginatedRequest { + @EncodeDefault + override val method: Method = Method.Defined.ResourcesTemplatesList +} + +/** + * The server's response to a [ListResourceTemplatesRequest] from the client. + * + * Returns the available resource templates along with pagination information if there are more results. + * + * @property resourceTemplates The list of available resource templates. Each template includes + * its URI template pattern, name, optional description, and information + * about the arguments it accepts. + * @property nextCursor An opaque token representing the pagination position after the last returned result. + * If present, there may be more results available. The client can pass this token + * in a subsequent request to fetch the next page. + * @property meta Optional metadata for this response. + */ +@Serializable +public data class ListResourceTemplatesResult( + val resourceTemplates: List, + override val nextCursor: String? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : ServerResult, + PaginatedResult diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/roots.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/roots.dsl.kt new file mode 100644 index 0000000..c1af201 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/roots.dsl.kt @@ -0,0 +1,59 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * Creates a [ListRootsRequest] using a type-safe DSL builder. + * + * ## Optional + * - [meta][ListRootsRequestBuilder.meta] - Metadata for the request + * + * Example with no parameters: + * ```kotlin + * val request = buildListRootsRequest { } + * ``` + * + * Example with metadata: + * ```kotlin + * val request = buildListRootsRequest { + * meta { + * put("context", "initialization") + * } + * } + * ``` + * + * @param block Configuration lambda for setting up the list roots request + * @return A configured [ListRootsRequest] instance + * @see ListRootsRequestBuilder + * @see ListRootsRequest + */ +@OptIn(ExperimentalContracts::class) +@ExperimentalMcpApi +public inline fun buildListRootsRequest(block: ListRootsRequestBuilder.() -> Unit): ListRootsRequest { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + return ListRootsRequestBuilder().apply(block).build() +} + +/** + * DSL builder for constructing [ListRootsRequest] instances. + * + * This builder retrieves the list of root URIs provided by the client. + * All fields are optional. + * + * ## Optional + * - [meta] - Metadata for the request + * + * @see buildListRootsRequest + * @see ListRootsRequest + */ +@McpDsl +public class ListRootsRequestBuilder @PublishedApi internal constructor() : RequestBuilder() { + @PublishedApi + override fun build(): ListRootsRequest { + val params = meta?.let { BaseRequestParams(meta = it) } + return ListRootsRequest(params) + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/roots.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/roots.kt new file mode 100644 index 0000000..7ee4f9e --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/roots.kt @@ -0,0 +1,85 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import kotlinx.serialization.EncodeDefault +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject + +/** + * Represents a root directory or file that the server can operate on. + * + * Roots define the top-level locations that a server has access to. They act as + * boundaries for the server's operations, ensuring it only accesses permitted locations. + * + * @property uri The URI identifying the root. This **must** start with `file://` for now. + * This restriction may be relaxed in future versions of the protocol to allow other URI schemes. + * Examples: "file:///home/user/projects", "file:///workspace/repo", "file:///C:/Documents" + * @property name An optional name for the root. This can be used to provide a human-readable + * identifier for the root, which may be useful for display purposes or for + * referencing the root in other parts of the application. + * @property meta Optional metadata for this root. + */ +@Serializable +public data class Root( + val uri: String, + val name: String? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : WithMeta { + init { + require(uri.startsWith("file://")) { + "Root URI must start with 'file://', got: $uri" + } + } +} + +// ============================================================================ +// roots/list +// ============================================================================ + +/** + * Sent from the server to request a list of root URIs from the client. + * + * Roots allow servers to ask for specific directories or files to operate on. + * A common example for roots is providing a set of repositories or directories + * a server should operate on. + * + * This request is typically used when the server needs to understand the file system + * structure or access specific locations that the client has permission to read from. + * + * **Note:** Unlike most other requests, this is sent from the **server to the client**, + * not from client to server. The client must support the `roots` capability with + * `listChanged = true` to receive these requests. + * + * @property params Optional request parameters containing metadata. + */ +@Serializable +public data class ListRootsRequest(override val params: BaseRequestParams? = null) : ServerRequest { + @OptIn(ExperimentalSerializationApi::class) + @EncodeDefault + override val method: Method = Method.Defined.RootsList + + /** Convenience accessor for [params]'s metadata. */ + public val meta: RequestMeta? + get() = params?.meta +} + +/** + * The client's response to a [ListRootsRequest] from the server. + * + * This result contains an array of Root objects, each representing a root directory + * or file that the server can operate on. Roots define the boundaries of what + * the server is allowed to access. + * + * @property roots The list of root URIs that the server can access. + * Each root represents a top-level directory, file, or other location + * that the server has permission to work with. + * @property meta Optional metadata for this response. + */ +@Serializable +public data class ListRootsResult( + val roots: List, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : ClientResult diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.dsl.kt new file mode 100644 index 0000000..4752ff5 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.dsl.kt @@ -0,0 +1,394 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonObjectBuilder +import kotlinx.serialization.json.buildJsonObject +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * Creates a [CreateMessageRequest] using a type-safe DSL builder. + * + * ## Required + * - [maxTokens][CreateMessageRequestBuilder.maxTokens] - Maximum number of tokens to generate + * - [messages][CreateMessageRequestBuilder.messages] - List of conversation messages + * + * ## Optional + * - [systemPrompt][CreateMessageRequestBuilder.systemPrompt] - System-level instructions + * - [context][CreateMessageRequestBuilder.context] - Context inclusion settings + * - [temperature][CreateMessageRequestBuilder.temperature] - Sampling temperature + * - [stopSequences][CreateMessageRequestBuilder.stopSequences] - Sequences that stop generation + * - [preferences][CreateMessageRequestBuilder.preferences] - Model selection preferences + * - [metadata][CreateMessageRequestBuilder.metadata] - Additional metadata + * - [meta][CreateMessageRequestBuilder.meta] - Request metadata + * + * Example: + * ```kotlin + * val request = buildCreateMessageRequest { + * maxTokens = 1000 + * systemPrompt = "You are a helpful assistant" + * messages { + * user { "What is the capital of France?" } + * assistant { "The capital of France is Paris." } + * user { "What about Germany?" } + * } + * } + * ``` + * + * Example with preferences: + * ```kotlin + * val request = buildCreateMessageRequest { + * maxTokens = 500 + * temperature = 0.7 + * preferences( + * hints = listOf("claude-3-sonnet"), + * intelligence = 0.8 + * ) + * messages { + * user { "Explain quantum computing" } + * } + * } + * ``` + * + * @param block Configuration lambda for setting up the create message request + * @return A configured [CreateMessageRequest] instance + * @see CreateMessageRequestBuilder + * @see CreateMessageRequest + */ +@OptIn(ExperimentalContracts::class) +@ExperimentalMcpApi +public inline fun buildCreateMessageRequest(block: CreateMessageRequestBuilder.() -> Unit): CreateMessageRequest { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + return CreateMessageRequestBuilder().apply(block).build() +} + +/** + * DSL builder for constructing [CreateMessageRequest] instances. + * + * This builder creates LLM sampling requests with conversation history. + * + * ## Required + * - [maxTokens] - Maximum number of tokens to generate + * - [messages] - List of conversation messages + * + * ## Optional + * - [systemPrompt] - System-level instructions + * - [context] - Context inclusion settings + * - [temperature] - Sampling temperature (0.0-1.0) + * - [stopSequences] - Sequences that stop generation + * - [preferences] - Model selection preferences + * - [metadata] - Additional metadata + * - [meta] - Request metadata + * + * @see buildCreateMessageRequest + * @see CreateMessageRequest + */ +@McpDsl +public class CreateMessageRequestBuilder @PublishedApi internal constructor() : RequestBuilder() { + /** Maximum number of tokens to generate. This is a required field. */ + public var maxTokens: Int? = null + + /** Optional system-level instructions for the model. */ + public var systemPrompt: String? = null + + /** Optional context inclusion settings. */ + public var context: IncludeContext? = null + + /** Optional sampling temperature (0.0-1.0). Lower values are more deterministic. */ + public var temperature: Double? = null + + /** Optional sequences that will stop generation when encountered. */ + public var stopSequences: List? = null + + private var messages: List? = null + private var preferences: ModelPreferences? = null + private var metadata: JsonObject? = null + + /** + * Sets messages directly from a list. + * + * Example: + * ```kotlin + * messages(listOf( + * SamplingMessage(Role.User, listOf(TextContent("Hello"))), + * SamplingMessage(Role.Assistant, listOf(TextContent("Hi!"))) + * )) + * ``` + */ + public fun messages(messages: List) { + this.messages = messages + } + + /** + * Sets messages using a DSL builder. + * + * This is the recommended way to define conversation messages. + * + * Example: + * ```kotlin + * messages { + * user { "What is 2+2?" } + * assistant { "2+2 equals 4." } + * user { "What about 3+3?" } + * } + * ``` + */ + public fun messages(block: SamplingMessageBuilder.() -> Unit) { + this.messages = SamplingMessageBuilder().apply(block).build() + } + + /** + * Sets model preferences directly. + */ + public fun preferences(value: ModelPreferences) { + this.preferences = value + } + + /** + * Sets model selection preferences using individual parameters. + * + * Example: + * ```kotlin + * preferences( + * hints = listOf("claude-3-sonnet"), + * intelligence = 0.9, + * speed = 0.5 + * ) + * ``` + * + * @param hints Model name hints for selection + * @param cost Cost optimization priority (0.0-1.0) + * @param speed Speed optimization priority (0.0-1.0) + * @param intelligence Intelligence optimization priority (0.0-1.0) + */ + public fun preferences( + hints: List? = null, + cost: Double? = null, + speed: Double? = null, + intelligence: Double? = null, + ) { + this.preferences = ModelPreferences( + hints = hints?.map { ModelHint(it) }, + costPriority = cost, + speedPriority = speed, + intelligencePriority = intelligence, + ) + } + + /** + * Sets additional metadata using a DSL builder. + * + * Example: + * ```kotlin + * metadata { + * put("userId", "123") + * put("sessionId", "abc") + * } + * ``` + */ + public fun metadata(block: JsonObjectBuilder.() -> Unit) { + this.metadata = buildJsonObject(block) + } + + @PublishedApi + override fun build(): CreateMessageRequest { + val maxTokens = requireNotNull(maxTokens) { + "Missing required field 'maxTokens'. Example: maxTokens = 1000" + } + val messages = requireNotNull(messages) { + "Missing required field 'messages'. Use messages { user { \"text\" } }" + } + + val params = CreateMessageRequestParams( + maxTokens = maxTokens, + messages = messages, + modelPreferences = preferences, + systemPrompt = systemPrompt, + includeContext = context, + temperature = temperature, + stopSequences = stopSequences, + metadata = metadata, + meta = meta, + ) + return CreateMessageRequest(params) + } +} + +/** + * DSL builder for constructing lists of [SamplingMessage] instances. + * + * This builder creates a conversation history for LLM sampling requests. + * + * Example: + * ```kotlin + * messages { + * user { "Hello!" } + * assistant { "Hi! How can I help?" } + * user { "What's 2+2?" } + * } + * ``` + * + * @see SamplingMessage + * @see CreateMessageRequestBuilder.messages + */ +@McpDsl +public class SamplingMessageBuilder @PublishedApi internal constructor() { + private val messages = mutableListOf() + + /** + * Adds a user message with the specified content. + * + * Example: + * ```kotlin + * user(TextContent("Hello")) + * ``` + */ + public fun user(content: MediaContent) { + messages.add(SamplingMessage(Role.User, listOf(content))) + } + + /** + * Adds an assistant message with the specified content. + * + * Example: + * ```kotlin + * assistant(TextContent("Hi there!")) + * ``` + */ + public fun assistant(content: MediaContent) { + messages.add(SamplingMessage(Role.Assistant, listOf(content))) + } + + @PublishedApi + internal fun build(): List = messages +} + +/** + * Adds a user message with simple text content. + * + * Example: + * ```kotlin + * messages { + * user { "What is the weather today?" } + * } + * ``` + */ +@ExperimentalMcpApi +public fun SamplingMessageBuilder.user(block: () -> String): Unit = this.user(TextContent(text = block())) + +/** + * Adds a user message with text content using a DSL builder. + * + * Example: + * ```kotlin + * messages { + * userText { + * text = "Important message" + * annotations(priority = 1.0) + * } + * } + * ``` + */ +@ExperimentalMcpApi +public fun SamplingMessageBuilder.userText(block: TextContentBuilder.() -> Unit): Unit = + this.user(TextContentBuilder().apply(block).build()) + +/** + * Adds a user message with image content. + * + * Example: + * ```kotlin + * messages { + * userImage { + * data = base64ImageData + * mimeType = "image/png" + * } + * } + * ``` + */ +@ExperimentalMcpApi +public fun SamplingMessageBuilder.userImage(block: ImageContentBuilder.() -> Unit): Unit = + this.user(ImageContentBuilder().apply(block).build()) + +/** + * Adds a user message with audio content. + * + * Example: + * ```kotlin + * messages { + * userAudio { + * data = base64AudioData + * mimeType = "audio/wav" + * } + * } + * ``` + */ +@ExperimentalMcpApi +public fun SamplingMessageBuilder.userAudio(block: AudioContentBuilder.() -> Unit): Unit = + this.user(AudioContentBuilder().apply(block).build()) + +/** + * Adds an assistant message with simple text content. + * + * Example: + * ```kotlin + * messages { + * assistant { "The weather is sunny today." } + * } + * ``` + */ +@ExperimentalMcpApi +public fun SamplingMessageBuilder.assistant(block: () -> String): Unit = this.assistant(TextContent(text = block())) + +/** + * Adds an assistant message with text content using a DSL builder. + * + * Example: + * ```kotlin + * messages { + * assistantText { + * text = "Here's the answer" + * annotations(priority = 0.8) + * } + * } + * ``` + */ +@ExperimentalMcpApi +public fun SamplingMessageBuilder.assistantText(block: TextContentBuilder.() -> Unit): Unit = + this.assistant(TextContentBuilder().apply(block).build()) + +/** + * Adds an assistant message with image content. + * + * Example: + * ```kotlin + * messages { + * assistantImage { + * data = generatedImageData + * mimeType = "image/jpeg" + * } + * } + * ``` + */ +@ExperimentalMcpApi +public fun SamplingMessageBuilder.assistantImage(block: ImageContentBuilder.() -> Unit): Unit = + this.assistant(ImageContentBuilder().apply(block).build()) + +/** + * Adds an assistant message with audio content. + * + * Example: + * ```kotlin + * messages { + * assistantAudio { + * data = generatedAudioData + * mimeType = "audio/mpeg" + * } + * } + * ``` + */ +@ExperimentalMcpApi +public fun SamplingMessageBuilder.assistantAudio(block: AudioContentBuilder.() -> Unit): Unit = + this.assistant(AudioContentBuilder().apply(block).build()) diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.kt new file mode 100644 index 0000000..7cc2685 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.kt @@ -0,0 +1,354 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import kotlinx.serialization.EncodeDefault +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject +import kotlin.jvm.JvmInline + +/** + * Hints to use for model selection. + */ +@Serializable +public data class ModelHint( + /** + * A hint for a model name. + */ + val name: String?, +) + +/** + * The server's preferences for model selection, requested of the client during sampling. + * + * Because LLMs can vary along multiple dimensions, choosing the "best" model is rarely straightforward. + * Different models excel in different areas—some are faster but less capable, others are more capable + * but more expensive, and so on. This interface allows servers to express their priorities across + * multiple dimensions to help clients make an appropriate selection for their use case. + * + * **Important:** These preferences are always advisory. The client MAY ignore them completely. + * It is also up to the client to decide how to interpret these preferences and how to balance + * them against other considerations. + * + * @property hints Optional hints to use for model selection. + * If multiple hints are specified, the client MUST evaluate them in order + * (such that the first match is taken). + * The client SHOULD prioritize these hints over the numeric priorities, + * but MAY still use the priorities to select from ambiguous matches. + * @property costPriority How much to prioritize cost when selecting a model. + * A value of 0.0 means cost is not important, while a value of 1.0 means + * cost is the most important factor. + * Should be between 0.0 and 1.0. + * @property speedPriority How much to prioritize sampling speed (latency) when selecting a model. + * A value of 0.0 means speed is not important, while a value of 1.0 means + * speed is the most important factor. + * Should be between 0.0 and 1.0. + * @property intelligencePriority How much to prioritize intelligence and capabilities when selecting a model. + * A value of 0.0 means intelligence is not important, while a value of 1.0 + * means intelligence is the most important factor. + * Should be between 0.0 and 1.0. + */ +@Serializable +public data class ModelPreferences( + val hints: List? = null, + val costPriority: Double? = null, + val speedPriority: Double? = null, + val intelligencePriority: Double? = null, +) { + init { + require(costPriority == null || costPriority in 0.0..1.0) { + "costPriority must be in 0.0 <= x <= 1.0 value range" + } + + require(speedPriority == null || speedPriority in 0.0..1.0) { + "costPriority must be in 0.0 <= x <= 1.0 value range" + } + + require(intelligencePriority == null || intelligencePriority in 0.0..1.0) { + "intelligencePriority must be in 0.0 <= x <= 1.0 value range" + } + } +} + +/** + * Describes a message issued to or received from an LLM API. + * + * Used in sampling requests to provide conversation context and history to the LLM. + * + * Under SEP-1577, [content] is a list of [SamplingMessageContent] blocks rather than a + * single block. On the wire, a list of size 1 is serialised as a single object (wire-compatible + * with pre-SEP single-block content); a list of size 2+ is serialised as a JSON array. + * + * @property role The role of the message sender (user, assistant, or system). + * @property content The content blocks of the message; MUST contain at least one block. + * @property meta Optional metadata reserved by MCP for clients and servers. + */ +@Serializable +public data class SamplingMessage( + val role: Role, + @Serializable(with = SamplingContentSerializer::class) + val content: List, + @SerialName("_meta") + val meta: JsonObject? = null, +) { + init { + require(content.isNotEmpty()) { "content must contain at least one block" } + } + + /** + * Convenience constructor for a single-block message. Wraps [content] in a + * singleton list so call sites can write `SamplingMessage(Role.User, TextContent("hi"))` + * without the explicit `listOf(...)`. + */ + public constructor( + role: Role, + content: SamplingMessageContent, + meta: JsonObject? = null, + ) : this(role, listOf(content), meta) +} + +// ============================================================================ +// sampling/createMessage +// ============================================================================ + +/** + * A request from the server to sample an LLM via the client. + * + * The client has full discretion over which model to select. The client should also + * inform the user before beginning sampling, to allow them to inspect the request + * (human in the loop) and decide whether to approve it. + * + * **Important:** This is a request from **server to client**, not client to server. + * The server is asking the client to use its LLM to generate a completion. + * + * **Human-in-the-loop:** Clients should: + * 1. Show the sampling request to the user before executing it + * 2. Allow the user to approve or reject the request + * 3. Show the generated response to the user before sending it back to the server + * + * @property params The parameters for the sampling request, including messages and model preferences. + */ +@Serializable +public data class CreateMessageRequest(override val params: CreateMessageRequestParams) : ServerRequest { + @OptIn(ExperimentalSerializationApi::class) + @EncodeDefault + override val method: Method = Method.Defined.SamplingCreateMessage + + /** + * The requested maximum number of tokens to sample (to prevent runaway completions). + */ + public val maxTokens: Int + get() = params.maxTokens + + /** + * The messages to use as context for sampling. + */ + public val messages: List + get() = params.messages + + /** + * The server's preferences for which model to select. + */ + public val modelPreferences: ModelPreferences? + get() = params.modelPreferences + + /** + * An optional system prompt the server wants to use for sampling. + */ + public val systemPrompt: String? + get() = params.systemPrompt + + /** + * A request to include context from one or more MCP servers (including the caller), to be attached to the prompt. + */ + public val includeContext: IncludeContext? + get() = params.includeContext + + /** + * Temperature parameter for sampling (typically 0.0-2.0). + */ + public val temperature: Double? + get() = params.temperature + + /** + * List of sequences that will stop generation if encountered. + */ + public val stopSequences: List? + get() = params.stopSequences + + /** + * Metadata to pass through to the LLM provider. The format of this metadata is provider-specific. + */ + public val metadata: JsonObject? + get() = params.metadata + + /** + * Task metadata, if the caller is requesting task-augmented execution. + */ + public val task: TaskMetadata? + get() = params.task + + /** + * Metadata for this request. May include a progressToken for out-of-band progress notifications. + */ + public val meta: RequestMeta? + get() = params.meta +} + +/** + * Parameters for a sampling/createMessage request. + * + * @property maxTokens The requested maximum number of tokens to sample (to prevent runaway completions). + * The client MAY choose to sample fewer tokens than the requested maximum. + * @property messages The messages to use as context for sampling. Typically includes conversation history + * and the current user message. + * @property modelPreferences The server's preferences for which model to select. + * The client MAY ignore these preferences and choose any model. + * @property systemPrompt An optional system prompt the server wants to use for sampling. + * The client MAY modify or omit this prompt. + * @property includeContext A request to include context from one or more MCP servers (including the caller), + * to be attached to the prompt. The client MAY ignore this request. + * - `none`: Don't include any server context + * - `thisServer`: Include context only from the requesting server + * - `allServers`: Include context from all connected MCP servers + * @property temperature Optional temperature parameter for sampling (typically 0.0-2.0). + * Higher values make output more random, lower values make it more deterministic. + * @property stopSequences Optional list of sequences that will stop generation if encountered. + * @property metadata Optional metadata to pass through to the LLM provider. + * The format of this metadata is provider-specific. + * @property tools Optional list of tools the model may use during generation. + * The client MUST return an error if this field is present but the client did not advertise + * [ClientCapabilities.Sampling.tools]. + * @property toolChoice Optional policy controlling how the model uses the provided [tools]. + * The client MUST return an error if this field is present but the client did not advertise + * [ClientCapabilities.Sampling.tools]. + * @property task If set, requests task-augmented execution. A receiver that supports it returns + * a [CreateTaskResult] (result via `tasks/result`); otherwise it returns a [CreateMessageResult] directly. + * @property meta Optional metadata for this request. May include a progressToken for + * out-of-band progress notifications. + */ +@Serializable +public data class CreateMessageRequestParams( + val maxTokens: Int, + val messages: List, + val modelPreferences: ModelPreferences? = null, + val systemPrompt: String? = null, + val includeContext: IncludeContext? = null, + val temperature: Double? = null, + val stopSequences: List? = null, + val metadata: JsonObject? = null, + val tools: List? = null, + val toolChoice: ToolChoice? = null, + val task: TaskMetadata? = null, + @SerialName("_meta") + override val meta: RequestMeta? = null, +) : RequestParams + +/** + * Specifies which MCP server context to include in the sampling request. + */ +@Serializable +public enum class IncludeContext { + /** Don't include any server context */ + @SerialName("none") + None, + + /** Include context only from the requesting server */ + @SerialName("thisServer") + ThisServer, + + /** Include context from all connected MCP servers */ + @SerialName("allServers") + AllServers, +} + +/** + * The client's response to a [CreateMessageRequest] from the server. + * + * The client should inform the user before returning the sampled message, to allow them + * to inspect the response (human in the loop) and decide whether to allow the server to see it. + * + * @property role The role of the message sender. Typically [Role.Assistant] for LLM-generated responses. + * @property content The generated content blocks; at least one block is required. + * @property model The name of the model that generated the message (e.g., "claude-3-opus-20240229"). + * @property stopReason The reason why sampling stopped, if known. + * Common values: [StopReason.EndTurn], [StopReason.StopSequence], [StopReason.MaxTokens], + * [StopReason.ToolUse]. + * @property meta Optional metadata for this response. + */ +@Serializable +public data class CreateMessageResult( + val role: Role, + @Serializable(with = SamplingContentSerializer::class) + val content: List, + val model: String, + val stopReason: StopReason? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : ClientResult { + init { + require(content.isNotEmpty()) { "content must contain at least one block" } + } + + /** + * Convenience constructor for a single-block response. Wraps [content] in a + * singleton list so call sites can write + * `CreateMessageResult(Role.Assistant, TextContent("ok"), "model-name")` + * without the explicit `listOf(...)`. + */ + public constructor( + role: Role, + content: SamplingMessageContent, + model: String, + stopReason: StopReason? = null, + meta: JsonObject? = null, + ) : this(role, listOf(content), model, stopReason, meta) +} + +/** + * The reason why the LLM stopped generating tokens. + * + * @property value the string representation of this stop reason + */ +@JvmInline +@Serializable +public value class StopReason(public val value: String) { + /** + * @property EndTurn generation ended naturally + * @property StopSequence a stop sequence was encountered + * @property MaxTokens the maximum token limit was reached + * @property ToolUse the assistant issued a tool call (see SEP-1577) + */ + public companion object { + public val EndTurn: StopReason = StopReason("endTurn") + public val StopSequence: StopReason = StopReason("stopSequence") + public val MaxTokens: StopReason = StopReason("maxTokens") + public val ToolUse: StopReason = StopReason("toolUse") + } +} + +/** + * Controls tool-selection behaviour for [CreateMessageRequest] under SEP-1577. + * + * @property mode Tool-use policy: + * - [Mode.Auto]: the model decides whether to use tools (default when [mode] is absent) + * - [Mode.Required]: the model MUST use at least one tool before completing + * - [Mode.None]: the model MUST NOT use any tool + */ +@Serializable +public data class ToolChoice(public val mode: Mode? = null) { + /** + * Allowed values of [ToolChoice.mode] per SEP-1577. + */ + @Serializable + public enum class Mode { + @SerialName("auto") + Auto, + + @SerialName("required") + Required, + + @SerialName("none") + None, + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/schema.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/schema.kt new file mode 100644 index 0000000..0982500 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/schema.kt @@ -0,0 +1,302 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import kotlinx.serialization.EncodeDefault +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Restricted schema definition for elicitation request properties. + * + * Supports primitive types (string, number, integer, boolean) and enum schemas + * (single-select and multi-select). Only flat, top-level properties are allowed — + * arbitrary nested objects or arrays are not supported. + * + * Implementations: [StringSchema], [NumberSchemaDefinition], [BooleanSchema], [EnumSchemaDefinition]. + */ +@Serializable(with = PrimitiveSchemaDefinitionSerializer::class) +public sealed interface PrimitiveSchemaDefinition + +/** + * Defines a string-typed property in an elicitation schema. + * + * @property title Optional display title for the field. + * @property description Optional description for the field. + * @property minLength Minimum string length. + * @property maxLength Maximum string length. + * @property format Optional format constraint (e.g., email, URI, date). + * @property default Optional default value. + */ +@Serializable +public data class StringSchema( + val title: String? = null, + val description: String? = null, + val minLength: Int? = null, + val maxLength: Int? = null, + val format: StringSchemaFormat? = null, + val default: String? = null, +) : PrimitiveSchemaDefinition { + /** JSON Schema type discriminator, always `"string"`. */ + @EncodeDefault + public val type: String = "string" +} + +/** + * Supported format constraints for [StringSchema]. + */ +@Serializable +public enum class StringSchemaFormat { + @SerialName("email") + Email, + + @SerialName("uri") + Uri, + + @SerialName("date") + Date, + + @SerialName("date-time") + DateTime, +} + +/** + * Defines a numeric property in an elicitation schema. + * + * Implementations: [IntegerSchema] (`"integer"`), [DoubleSchema] (`"number"`). + */ +@Serializable +public sealed interface NumberSchemaDefinition : PrimitiveSchemaDefinition + +/** + * Defines an integer-typed property in an elicitation schema. + * + * @property title Optional display title for the field. + * @property description Optional description for the field. + * @property minimum Minimum allowed value. + * @property maximum Maximum allowed value. + * @property default Optional default value. + */ +@Serializable +public data class IntegerSchema( + val title: String? = null, + val description: String? = null, + val minimum: Int? = null, + val maximum: Int? = null, + val default: Int? = null, +) : NumberSchemaDefinition { + /** JSON Schema type discriminator, always `"integer"`. */ + @EncodeDefault + val type: String = "integer" +} + +/** + * Defines a floating-point number property in an elicitation schema. + * + * @property title Optional display title for the field. + * @property description Optional description for the field. + * @property minimum Minimum allowed value. + * @property maximum Maximum allowed value. + * @property default Optional default value. + */ +@Serializable +public data class DoubleSchema( + val title: String? = null, + val description: String? = null, + val minimum: Double? = null, + val maximum: Double? = null, + val default: Double? = null, +) : NumberSchemaDefinition { + /** JSON Schema type discriminator, always `"number"`. */ + @EncodeDefault + val type: String = "number" +} + +/** + * Defines a boolean-typed property in an elicitation schema. + * + * @property title Optional display title for the field. + * @property description Optional description for the field. + * @property default Optional default value. + */ +@Serializable +public data class BooleanSchema( + val title: String? = null, + val description: String? = null, + val default: Boolean? = null, +) : PrimitiveSchemaDefinition { + /** JSON Schema type discriminator, always `"boolean"`. */ + @EncodeDefault + val type: String = "boolean" +} + +/** + * Defines an enumeration property in an elicitation schema. + * + * Implementations: [SingleSelectEnumSchema], [MultiSelectEnumSchema], [LegacyTitledEnumSchema]. + */ +@Serializable +public sealed interface EnumSchemaDefinition : PrimitiveSchemaDefinition + +/** + * Represents an enum option with a value and a display label. + * + * Used in [TitledSingleSelectEnumSchema] and [TitledMultiSelectEnumSchema]. + * + * @property const The enum value. + * @property title Display label for this option. + */ +@Serializable +public data class EnumOption(val const: String, val title: String) + +/** + * Defines a single-selection enumeration property. + * + * Implementations: [UntitledSingleSelectEnumSchema], [TitledSingleSelectEnumSchema]. + */ +@Serializable +public sealed interface SingleSelectEnumSchema : EnumSchemaDefinition + +/** + * Defines a single-selection enumeration without display titles for options. + * + * @property title Optional display title for the field. + * @property description Optional description for the field. + * @property enumValues Array of enum values to choose from. + * @property default Optional default value. + */ +@Serializable +public data class UntitledSingleSelectEnumSchema( + val title: String? = null, + val description: String? = null, + @SerialName("enum") + val enumValues: List, + val default: String? = null, +) : SingleSelectEnumSchema { + /** JSON Schema type discriminator, always `"string"`. */ + @EncodeDefault + val type: String = "string" +} + +/** + * Defines a single-selection enumeration with display titles for each option. + * + * @property title Optional display title for the field. + * @property description Optional description for the field. + * @property oneOf Array of enum options with values and display labels. + * @property default Optional default value. + */ +@Serializable +public data class TitledSingleSelectEnumSchema( + val title: String? = null, + val description: String? = null, + val oneOf: List, + val default: String? = null, +) : SingleSelectEnumSchema { + /** JSON Schema type discriminator, always `"string"`. */ + @EncodeDefault + val type: String = "string" +} + +/** + * Defines a single-selection enumeration with display names via the deprecated `enumNames` array. + * + * Use [TitledSingleSelectEnumSchema] instead. This class will be removed in a future version. + * + * @property title Optional display title for the field. + * @property description Optional description for the field. + * @property enumValues Array of enum values to choose from. + * @property enumNames Display names for enum values. Non-standard according to JSON Schema 2020-12. + * @property default Optional default value. + */ +@Deprecated("Use TitledSingleSelectEnumSchema instead") +@Serializable +public data class LegacyTitledEnumSchema( + val title: String? = null, + val description: String? = null, + @SerialName("enum") + val enumValues: List, + val enumNames: List? = null, + val default: String? = null, +) : EnumSchemaDefinition { + /** JSON Schema type discriminator, always `"string"`. */ + @EncodeDefault + val type: String = "string" +} + +/** + * Defines a multiple-selection enumeration property. + * + * Implementations: [UntitledMultiSelectEnumSchema], [TitledMultiSelectEnumSchema]. + */ +@Serializable +public sealed interface MultiSelectEnumSchema : EnumSchemaDefinition + +/** + * Defines a multiple-selection enumeration without display titles for options. + * + * @property title Optional display title for the field. + * @property description Optional description for the field. + * @property minItems Minimum number of items to select. + * @property maxItems Maximum number of items to select. + * @property items Schema for the array items. + * @property default Optional default value. + */ +@Serializable +public data class UntitledMultiSelectEnumSchema( + val title: String? = null, + val description: String? = null, + val minItems: Int? = null, + val maxItems: Int? = null, + val items: Items, + val default: List? = null, +) : MultiSelectEnumSchema { + /** JSON Schema type discriminator, always `"array"`. */ + @EncodeDefault + val type: String = "array" + + /** + * Schema for the array items with plain enum values. + * + * @property enumValues Array of enum values to choose from. + */ + @Serializable + public data class Items( + @SerialName("enum") + val enumValues: List, + ) { + /** JSON Schema type discriminator, always `"string"`. */ + @EncodeDefault + val type: String = "string" + } +} + +/** + * Defines a multiple-selection enumeration with display titles for each option. + * + * @property title Optional display title for the field. + * @property description Optional description for the field. + * @property minItems Minimum number of items to select. + * @property maxItems Maximum number of items to select. + * @property items Schema for array items with enum options and display labels. + * @property default Optional default value. + */ +@Serializable +public data class TitledMultiSelectEnumSchema( + val title: String? = null, + val description: String? = null, + val minItems: Int? = null, + val maxItems: Int? = null, + val items: Items, + val default: List? = null, +) : MultiSelectEnumSchema { + /** JSON Schema type discriminator, always `"array"`. */ + @EncodeDefault + val type: String = "array" + + /** + * Schema for array items with enum options and display labels. + * + * @property anyOf Array of enum options with values and display labels. + */ + @Serializable + public data class Items(val anyOf: List) +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt new file mode 100644 index 0000000..36b7e37 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt @@ -0,0 +1,600 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.github.oshai.kotlinlogging.KotlinLogging +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonContentPolymorphicSerializer +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.longOrNull + +private val logger = KotlinLogging.logger {} + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/** + * Safely extracts the method field from a JSON element. + * Returns null if the method field is not present. + */ +private fun JsonElement.getMethodOrNull(): String? = jsonObject["method"]?.jsonPrimitive?.content + +/** + * Safely extracts the type field from a JSON element. + * Returns null if the type field is not present. + */ +private fun JsonElement.getTypeOrNull(): String? = jsonObject["type"]?.jsonPrimitive?.content + +/** + * Extracts the type field from a JSON element. + * Throws [SerializationException] if the type field is not present. + */ +private fun JsonElement.getType(): String = requireNotNull(getTypeOrNull()) { "Missing required 'type' field" } + +// ============================================================================ +// Method Serializer +// ============================================================================ + +/** + * Custom serializer for [Method] that handles both defined and custom methods. + * Defined methods are deserialized to [Method.Defined], while unknown methods + * are deserialized to [Method.Custom]. + */ +internal object MethodSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("io.modelcontextprotocol.kotlin.sdk.types.Method", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: Method) { + encoder.encodeString(value.value) + } + + override fun deserialize(decoder: Decoder): Method = Method.from(decoder.decodeString()) +} + +// ============================================================================ +// Reference Serializers +// ============================================================================ + +/** + * Polymorphic serializer for [Reference] types. + * Determines the concrete type based on the "type" field in JSON. + */ +internal object ReferencePolymorphicSerializer : JsonContentPolymorphicSerializer(Reference::class) { + override fun selectDeserializer(element: JsonElement): DeserializationStrategy = + when (element.getType()) { + ReferenceType.Prompt.value -> PromptReference.serializer() + ReferenceType.ResourceTemplate.value -> ResourceTemplateReference.serializer() + else -> throw SerializationException("Unknown reference type: ${element.getTypeOrNull()}") + } +} + +// ============================================================================ +// Content Serializers +// ============================================================================ + +/** + * Polymorphic serializer for [ContentBlock] types. + * Determines the concrete type based on the "type" field in JSON. + */ +internal object ContentBlockPolymorphicSerializer : + JsonContentPolymorphicSerializer(ContentBlock::class) { + override fun selectDeserializer(element: JsonElement): DeserializationStrategy = + when (element.getType()) { + ContentTypes.TEXT.value -> TextContent.serializer() + ContentTypes.IMAGE.value -> ImageContent.serializer() + ContentTypes.AUDIO.value -> AudioContent.serializer() + ContentTypes.RESOURCE_LINK.value -> ResourceLink.serializer() + ContentTypes.EMBEDDED_RESOURCE.value -> EmbeddedResource.serializer() + else -> throw SerializationException("Unknown content block type: ${element.getTypeOrNull()}") + } +} + +/** + * Polymorphic serializer for [MediaContent] types (text, image, audio). + * Determines the concrete type based on the "type" field in JSON. + */ +internal object MediaContentPolymorphicSerializer : + JsonContentPolymorphicSerializer(MediaContent::class) { + override fun selectDeserializer(element: JsonElement): DeserializationStrategy = + when (element.getType()) { + ContentTypes.TEXT.value -> TextContent.serializer() + ContentTypes.IMAGE.value -> ImageContent.serializer() + ContentTypes.AUDIO.value -> AudioContent.serializer() + else -> throw SerializationException("Unknown media content type: ${element.getTypeOrNull()}") + } +} + +/** + * Polymorphic serializer for [SamplingMessageContent] types. + * Determines the concrete type based on the "type" field in JSON. + */ +internal object SamplingMessageContentPolymorphicSerializer : + JsonContentPolymorphicSerializer(SamplingMessageContent::class) { + override fun selectDeserializer(element: JsonElement): DeserializationStrategy = + when (element.getType()) { + ContentTypes.TEXT.value -> TextContent.serializer() + + ContentTypes.IMAGE.value -> ImageContent.serializer() + + ContentTypes.AUDIO.value -> AudioContent.serializer() + + ContentTypes.TOOL_USE.value -> ToolUseContent.serializer() + + ContentTypes.TOOL_RESULT.value -> ToolResultContent.serializer() + + else -> throw SerializationException( + "Unknown sampling message content type: ${element.getTypeOrNull()}", + ) + } +} + +/** + * Wire-format serializer for `List` honouring SEP-1577's + * single-object-or-array content shape. + * + * **Why this exists.** SEP-1577 widens the `content` field of [SamplingMessage] and + * [CreateMessageResult] from a single content block to either a single block or an + * array of blocks. Pre-SEP peers only understand a single object on the wire; post-SEP + * peers may send arrays when a turn carries multiple blocks (e.g. an assistant reply + * containing `[TextContent, ToolUseContent]` during a tool-loop step). + * + * The Kotlin API uses `List` uniformly to avoid branching in + * caller code. This serializer bridges the type difference: + * + * - **Read:** accepts both shapes; a single JSON object becomes `listOf(block)`, an + * array becomes the decoded list. + * - **Write:** size-1 list → single object (wire-compatible with pre-SEP peers); + * size≥2 → array; empty list throws (sampling content has no valid empty meaning + * per spec). + */ +internal object SamplingContentSerializer : KSerializer> { + + private val listSerializer = ListSerializer(SamplingMessageContentPolymorphicSerializer) + + override val descriptor: SerialDescriptor = listSerializer.descriptor + + override fun serialize(encoder: Encoder, value: List) { + check(value.isNotEmpty()) { "content must contain at least one block" } + val jsonEncoder = encoder as? JsonEncoder + ?: throw SerializationException("SamplingContentSerializer requires a Json encoder") + if (value.size == 1) { + jsonEncoder.encodeJsonElement( + jsonEncoder.json.encodeToJsonElement( + SamplingMessageContentPolymorphicSerializer, + value[0], + ), + ) + } else { + jsonEncoder.encodeJsonElement( + jsonEncoder.json.encodeToJsonElement(listSerializer, value), + ) + } + } + + override fun deserialize(decoder: Decoder): List { + val jsonDecoder = decoder as? JsonDecoder + ?: throw SerializationException("SamplingContentSerializer requires a Json decoder") + return decodeFromElement(jsonDecoder, jsonDecoder.decodeJsonElement()) + } + + private fun decodeFromElement(jsonDecoder: JsonDecoder, element: JsonElement): List = + when (element) { + is JsonArray -> { + if (element.isEmpty()) { + throw SerializationException("content must contain at least one block") + } + jsonDecoder.json.decodeFromJsonElement(listSerializer, element) + } + + is JsonObject -> listOf( + jsonDecoder.json.decodeFromJsonElement( + SamplingMessageContentPolymorphicSerializer, + element, + ), + ) + + else -> throw SerializationException( + "content must be a JSON object or array of objects, got $element", + ) + } +} + +// ============================================================================ +// Resource Serializers +// ============================================================================ + +/** + * Polymorphic serializer for [ResourceContents] types. + * Determines the concrete type based on the presence of "text" or "blob" fields. + */ +internal object ResourceContentsPolymorphicSerializer : + JsonContentPolymorphicSerializer(ResourceContents::class) { + override fun selectDeserializer(element: JsonElement): DeserializationStrategy { + val jsonObject = element.jsonObject + return when { + "text" in jsonObject -> TextResourceContents.serializer() + "blob" in jsonObject -> BlobResourceContents.serializer() + else -> UnknownResourceContents.serializer() + } + } +} + +// ============================================================================ +// Request Serializers +// ============================================================================ + +@OptIn(ExperimentalMcpApi::class) +private val clientRequestDeserializers: Map> by lazy { + mapOf( + Method.Defined.CompletionComplete.value to CompleteRequest.serializer(), + Method.Defined.ServerDiscover.value to DiscoverRequest.serializer(), + Method.Defined.Initialize.value to InitializeRequest.serializer(), + Method.Defined.Ping.value to PingRequest.serializer(), + Method.Defined.LoggingSetLevel.value to SetLevelRequest.serializer(), + Method.Defined.PromptsGet.value to GetPromptRequest.serializer(), + Method.Defined.PromptsList.value to ListPromptsRequest.serializer(), + Method.Defined.ResourcesList.value to ListResourcesRequest.serializer(), + Method.Defined.ResourcesRead.value to ReadResourceRequest.serializer(), + Method.Defined.ResourcesSubscribe.value to SubscribeRequest.serializer(), + Method.Defined.ResourcesUnsubscribe.value to UnsubscribeRequest.serializer(), + Method.Defined.ResourcesTemplatesList.value to ListResourceTemplatesRequest.serializer(), + Method.Defined.ToolsCall.value to CallToolRequest.serializer(), + Method.Defined.ToolsList.value to ListToolsRequest.serializer(), + Method.Defined.TasksGet.value to GetTaskRequest.serializer(), + Method.Defined.TasksResult.value to GetTaskPayloadRequest.serializer(), + Method.Defined.TasksList.value to ListTasksRequest.serializer(), + Method.Defined.TasksCancel.value to CancelTaskRequest.serializer(), + ) +} + +/** + * Selects the appropriate deserializer for client requests based on the method name. + * Returns null if the method is not a known client request method. + */ +internal fun selectClientRequestDeserializer(method: String): DeserializationStrategy? = + clientRequestDeserializers[method] + +private val serverRequestDeserializers: Map> by lazy { + mapOf( + Method.Defined.ElicitationCreate.value to ElicitRequest.serializer(), + Method.Defined.Ping.value to PingRequest.serializer(), + Method.Defined.RootsList.value to ListRootsRequest.serializer(), + Method.Defined.SamplingCreateMessage.value to CreateMessageRequest.serializer(), + Method.Defined.TasksGet.value to GetTaskRequest.serializer(), + Method.Defined.TasksResult.value to GetTaskPayloadRequest.serializer(), + Method.Defined.TasksList.value to ListTasksRequest.serializer(), + Method.Defined.TasksCancel.value to CancelTaskRequest.serializer(), + ) +} + +/** + * Selects the appropriate deserializer for server requests based on the method name. + * Returns null if the method is not a known server request method. + */ +internal fun selectServerRequestDeserializer(method: String): DeserializationStrategy? = + serverRequestDeserializers[method] + +/** + * Polymorphic serializer for [Request] types. + * Supports both client and server requests, as well as custom requests. + */ +internal object RequestPolymorphicSerializer : JsonContentPolymorphicSerializer(Request::class) { + override fun selectDeserializer(element: JsonElement): DeserializationStrategy { + val method = element.getMethodOrNull() ?: run { + logger.error { "Missing 'method' for Request: $element" } + throw SerializationException("Missing 'method' for Request: $element") + } + + return selectClientRequestDeserializer(method) + ?: selectServerRequestDeserializer(method) + ?: CustomRequest.serializer() + } +} + +// ============================================================================ +// Notification Serializers +// ============================================================================ + +private val clientNotificationDeserializers: Map> by lazy { + mapOf( + Method.Defined.NotificationsCancelled.value to CancelledNotification.serializer(), + Method.Defined.NotificationsProgress.value to ProgressNotification.serializer(), + Method.Defined.NotificationsInitialized.value to InitializedNotification.serializer(), + Method.Defined.NotificationsRootsListChanged.value to RootsListChangedNotification.serializer(), + Method.Defined.NotificationsTasksStatus.value to TaskStatusNotification.serializer(), + ) +} + +/** + * Selects the appropriate deserializer for client notifications based on the method name. + * Returns null if the method is not a known client notification method. + */ +private fun selectClientNotificationDeserializer(element: JsonElement): DeserializationStrategy? = + element.getMethodOrNull()?.let(clientNotificationDeserializers::get) + +private val serverNotificationDeserializers: Map> by lazy { + mapOf( + Method.Defined.NotificationsCancelled.value to CancelledNotification.serializer(), + Method.Defined.NotificationsProgress.value to ProgressNotification.serializer(), + Method.Defined.NotificationsMessage.value to LoggingMessageNotification.serializer(), + Method.Defined.NotificationsResourcesUpdated.value to ResourceUpdatedNotification.serializer(), + Method.Defined.NotificationsResourcesListChanged.value to ResourceListChangedNotification.serializer(), + Method.Defined.NotificationsToolsListChanged.value to ToolListChangedNotification.serializer(), + Method.Defined.NotificationsPromptsListChanged.value to PromptListChangedNotification.serializer(), + Method.Defined.NotificationsElicitationComplete.value to ElicitationCompleteNotification.serializer(), + Method.Defined.NotificationsTasksStatus.value to TaskStatusNotification.serializer(), + ) +} + +/** + * Selects the appropriate deserializer for server notifications based on the method name. + * Returns null if the method is not a known server notification method. + */ +internal fun selectServerNotificationDeserializer(element: JsonElement): DeserializationStrategy? = + element.getMethodOrNull()?.let(serverNotificationDeserializers::get) + +/** + * Polymorphic serializer for [Notification] types. + * Supports both client and server notifications, as well as custom notifications. + */ +internal object NotificationPolymorphicSerializer : + JsonContentPolymorphicSerializer(Notification::class) { + override fun selectDeserializer(element: JsonElement): DeserializationStrategy = + selectClientNotificationDeserializer(element) + ?: selectServerNotificationDeserializer(element) + ?: CustomNotification.serializer() +} + +/** + * Polymorphic serializer for [ClientNotification] types. + * Falls back to [CustomNotification] for unknown methods. + */ +internal object ClientNotificationPolymorphicSerializer : + JsonContentPolymorphicSerializer(ClientNotification::class) { + override fun selectDeserializer(element: JsonElement): DeserializationStrategy = + selectClientNotificationDeserializer(element) + ?: CustomNotification.serializer() +} + +/** + * Polymorphic serializer for [ServerNotification] types. + * Falls back to [CustomNotification] for unknown methods. + */ +internal object ServerNotificationPolymorphicSerializer : + JsonContentPolymorphicSerializer(ServerNotification::class) { + override fun selectDeserializer(element: JsonElement): DeserializationStrategy = + selectServerNotificationDeserializer(element) + ?: CustomNotification.serializer() +} + +// ============================================================================ +// Result Serializers +// ============================================================================ + +/** + * Selects the appropriate deserializer for empty results. + * Returns EmptyResult serializer if the JSON object is empty or contains only metadata. + */ +private fun selectEmptyResult(element: JsonElement): DeserializationStrategy? { + val jsonObject = element.jsonObject + return when { + jsonObject.isEmpty() || (jsonObject.size == 1 && "_meta" in jsonObject) -> EmptyResult.serializer() + else -> null + } +} + +/** + * Selects the appropriate deserializer for client results based on JSON content. + * Returns null if the structure doesn't match any known client result type. + */ +private fun selectClientResultDeserializer(element: JsonElement): DeserializationStrategy? { + val jsonObject = element.jsonObject + return when { + "model" in jsonObject && "role" in jsonObject -> CreateMessageResult.serializer() + "roots" in jsonObject -> ListRootsResult.serializer() + "action" in jsonObject -> ElicitResult.serializer() + "task" in jsonObject -> CreateTaskResult.serializer() + "tasks" in jsonObject -> ListTasksResult.serializer() + "taskId" in jsonObject -> GetTaskResult.serializer() + else -> null + } +} + +/** + * Selects the appropriate deserializer for server results based on JSON content. + * Returns null if the structure doesn't match any known server result type. + */ +@OptIn(ExperimentalMcpApi::class) +private fun selectServerResultDeserializer(element: JsonElement): DeserializationStrategy? { + val jsonObject = element.jsonObject + val isDiscoverResult = + "supportedVersions" in jsonObject && + "capabilities" in jsonObject && + "ttlMs" in jsonObject && + "cacheScope" in jsonObject + return when { + isDiscoverResult -> DiscoverResult.serializer() + "protocolVersion" in jsonObject && "capabilities" in jsonObject -> InitializeResult.serializer() + "completion" in jsonObject -> CompleteResult.serializer() + "tools" in jsonObject -> ListToolsResult.serializer() + "resources" in jsonObject -> ListResourcesResult.serializer() + "resourceTemplates" in jsonObject -> ListResourceTemplatesResult.serializer() + "prompts" in jsonObject -> ListPromptsResult.serializer() + "messages" in jsonObject -> GetPromptResult.serializer() + "contents" in jsonObject -> ReadResourceResult.serializer() + "content" in jsonObject -> CallToolResult.serializer() + "task" in jsonObject -> CreateTaskResult.serializer() + "tasks" in jsonObject -> ListTasksResult.serializer() + "taskId" in jsonObject -> GetTaskResult.serializer() + else -> null + } +} + +/** + * Polymorphic serializer for [RequestResult] types. + * Supports both client and server results. + * Throws [SerializationException] if the result type cannot be determined. + */ +internal object RequestResultPolymorphicSerializer : + JsonContentPolymorphicSerializer(RequestResult::class) { + override fun selectDeserializer(element: JsonElement): DeserializationStrategy = + selectClientResultDeserializer(element) + ?: selectServerResultDeserializer(element) + ?: selectEmptyResult(element) + ?: throw SerializationException("Cannot determine RequestResult type from JSON: ${element.jsonObject.keys}") +} + +/** + * Polymorphic serializer for [ClientResult] types. + * Throws [SerializationException] if the result type cannot be determined. + */ +internal object ClientResultPolymorphicSerializer : + JsonContentPolymorphicSerializer(ClientResult::class) { + override fun selectDeserializer(element: JsonElement): DeserializationStrategy = + selectClientResultDeserializer(element) + ?: selectEmptyResult(element) + ?: throw SerializationException("Cannot determine RequestResult type from JSON: ${element.jsonObject.keys}") +} + +/** + * Polymorphic serializer for [ServerResult] types. + * Throws [SerializationException] if the result type cannot be determined. + */ +internal object ServerResultPolymorphicSerializer : + JsonContentPolymorphicSerializer(ServerResult::class) { + override fun selectDeserializer(element: JsonElement): DeserializationStrategy = + selectServerResultDeserializer(element) + ?: selectEmptyResult(element) + ?: throw SerializationException("Cannot determine RequestResult type from JSON: ${element.jsonObject.keys}") +} + +// ============================================================================ +// JSON-RPC Serializers +// ============================================================================ + +/** + * Polymorphic serializer for [JSONRPCMessage] types. + * Determines the message type based on the presence of specific fields: + * - "error" -> JSONRPCError + * - "result" + "id" -> JSONRPCResponse + * - "result" -> JSONRPCEmptyMessage + * - "method" + "id" -> JSONRPCRequest + * - "method" -> JSONRPCNotification + */ +internal object JSONRPCMessagePolymorphicSerializer : + JsonContentPolymorphicSerializer(JSONRPCMessage::class) { + override fun selectDeserializer(element: JsonElement): DeserializationStrategy { + val jsonObj = element.jsonObject + return when { + "error" in jsonObj -> JSONRPCError.serializer() + "result" in jsonObj && "id" in jsonObj -> JSONRPCResponse.serializer() + "result" in jsonObj && jsonObj["result"]?.jsonObject?.isEmpty() == true -> JSONRPCEmptyMessage.serializer() + "method" in jsonObj && "id" in jsonObj -> JSONRPCRequest.serializer() + "method" in jsonObj -> JSONRPCNotification.serializer() + jsonObj.isEmpty() || jsonObj.keys == setOf("jsonrpc") -> JSONRPCEmptyMessage.serializer() + else -> throw SerializationException("Invalid JSONRPCMessage type: ${jsonObj.keys}") + } + } +} + +/** + * Polymorphic serializer for [RequestId] types. + * Supports both string and number IDs. + */ +internal object RequestIdPolymorphicSerializer : JsonContentPolymorphicSerializer(RequestId::class) { + override fun selectDeserializer(element: JsonElement): DeserializationStrategy = when { + element is JsonPrimitive && element.isString -> RequestId.StringId.serializer() + element is JsonPrimitive && element.longOrNull != null -> RequestId.NumberId.serializer() + else -> throw SerializationException("Invalid RequestId type: $element") + } +} + +// ============================================================================ +// ElicitationParams Serializers +// ============================================================================ + +internal object ElicitRequestParamsSerializer : JsonContentPolymorphicSerializer( + ElicitRequestParams::class, +) { + override fun selectDeserializer(element: JsonElement): DeserializationStrategy { + val mode = when (val modeElement = element.jsonObject["mode"]) { + null -> null + + is JsonPrimitive -> { + if (!modeElement.isString) { + throw SerializationException("Invalid 'mode' type: expected string but was: $modeElement") + } + modeElement.contentOrNull + } + + else -> throw SerializationException("Invalid 'mode' type: expected string but was: $modeElement") + } + return when (mode) { + "url" -> ElicitRequestURLParams.serializer() + "form", null -> ElicitRequestFormParams.serializer() + else -> throw SerializationException("Unsupported elicitation mode: '$mode'") + } + } +} + +// ============================================================================ +// PrimitiveSchemaDefinition Serializers +// ============================================================================ + +internal object PrimitiveSchemaDefinitionSerializer : JsonContentPolymorphicSerializer( + PrimitiveSchemaDefinition::class, +) { + override fun selectDeserializer(element: JsonElement): DeserializationStrategy { + val obj = element.jsonObject + val typeElement = obj["type"] + ?: throw SerializationException("Missing 'type' in PrimitiveSchemaDefinition: $element") + val type = if (typeElement is JsonPrimitive && typeElement.isString) { + typeElement.content + } else { + throw SerializationException("Expected 'type' to be a string but was: $typeElement") + } + return when (type) { + "boolean" -> BooleanSchema.serializer() + "integer" -> IntegerSchema.serializer() + "number" -> DoubleSchema.serializer() + "string" -> selectStringTypeDeserializer(obj) + "array" -> selectArrayTypeDeserializer(obj) + else -> throw SerializationException("Unknown PrimitiveSchemaDefinition type: '$type'") + } + } + + private fun selectArrayTypeDeserializer( + obj: Map, + ): DeserializationStrategy { + val items = obj["items"]?.jsonObject + return when { + items != null && "anyOf" in items -> TitledMultiSelectEnumSchema.serializer() + else -> UntitledMultiSelectEnumSchema.serializer() + } + } + + private fun selectStringTypeDeserializer( + obj: Map, + ): DeserializationStrategy = when { + "enumNames" in obj -> LegacyTitledEnumSchema.serializer() + "enum" in obj -> UntitledSingleSelectEnumSchema.serializer() + "oneOf" in obj -> TitledSingleSelectEnumSchema.serializer() + else -> StringSchema.serializer() + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tasks.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tasks.kt new file mode 100644 index 0000000..7363561 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tasks.kt @@ -0,0 +1,361 @@ +@file:OptIn(ExperimentalSerializationApi::class) + +package io.modelcontextprotocol.kotlin.sdk.types + +import kotlinx.serialization.EncodeDefault +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlin.jvm.JvmInline + +/** + * The key used in `_meta` to associate a message with a task. + */ +public const val RELATED_TASK_META_KEY: String = "io.modelcontextprotocol/related-task" + +/** + * Common fields shared by all types representing the task state. + */ +public sealed interface TaskFields { + /** The task identifier. */ + public val taskId: String + + /** Current task state. */ + public val status: TaskStatus + + /** Optional human-readable message describing the current task state. */ + public val statusMessage: String? + + /** ISO 8601 timestamp when the task was created. */ + public val createdAt: String + + /** ISO 8601 timestamp when the task was last updated. */ + public val lastUpdatedAt: String + + /** Actual retention duration from creation in milliseconds, or `null` for unlimited. */ + public val ttl: Long? + + /** Suggested polling interval in milliseconds. */ + public val pollInterval: Long? +} + +/** + * Data associated with a task. + * + * @property taskId The task identifier. + * @property status Current task state. + * @property statusMessage Optional human-readable message describing the current task state. + * This can provide context for any status, including reasons for "cancelled" status, + * summaries for "completed" status, or diagnostic information for "failed" status. + * @property createdAt ISO 8601 timestamp when the task was created. + * @property lastUpdatedAt ISO 8601 timestamp when the task was last updated. + * @property ttl Actual retention duration from creation in milliseconds, null for unlimited. + * @property pollInterval Suggested polling interval in milliseconds. + */ +@Serializable +public data class Task( + override val taskId: String, + override val status: TaskStatus, + override val statusMessage: String? = null, + override val createdAt: String, + override val lastUpdatedAt: String, + override val ttl: Long?, + override val pollInterval: Long? = null, +) : TaskFields + +/** + * Metadata for augmenting a request with task execution. + * Include this in the `task` field of the request parameters. + * + * @property ttl Requested duration in milliseconds to retain task from creation. + */ +@Serializable +public data class TaskMetadata(val ttl: Long? = null) + +/** + * The status of a task. + */ +@Serializable +public enum class TaskStatus { + @SerialName("working") + Working, + + @SerialName("input_required") + InputRequired, + + @SerialName("completed") + Completed, + + @SerialName("failed") + Failed, + + @SerialName("cancelled") + Cancelled, +} + +/** + * Metadata for associating messages with a task. + * Include this in the `_meta` field under the key `io.modelcontextprotocol/related-task`. + * + * @property taskId The task identifier this message is associated with. + */ +@Serializable +public data class RelatedTaskMetadata(val taskId: String) + +/** + * A response to a task-augmented request. + * + * @property task The task data associated with the response. + * @property meta Optional metadata for this response. + */ +@Serializable +public data class CreateTaskResult( + val task: Task, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : ClientResult, + ServerResult + +// ============================================================================ +// tasks/get +// ============================================================================ + +/** + * A request to retrieve the state of a task. + * + * This request can be sent by either side (both [ClientRequest] and [ServerRequest]). + * + * @property params The parameters specifying which task to retrieve. + */ +@Serializable +public data class GetTaskRequest(override val params: GetTaskRequestParams) : + ClientRequest, + ServerRequest { + @EncodeDefault + override val method: Method = Method.Defined.TasksGet + + /** + * The task identifier to query. + */ + public val taskId: String + get() = params.taskId + + /** + * Metadata for this request. May include a progressToken for out-of-band progress notifications. + */ + public val meta: RequestMeta? + get() = params.meta +} + +/** + * Parameters for a tasks/get request. + * + * @property taskId The task identifier to query. + * @property meta Optional metadata for this request. + */ +@Serializable +public data class GetTaskRequestParams( + val taskId: String, + @SerialName("_meta") + override val meta: RequestMeta? = null, +) : RequestParams + +/** + * The response to a tasks/get request. + * + * Contains all [Task] fields flattened into the result (intersection of Result and Task). + * + * @property taskId The task identifier. + * @property status Current task state. + * @property statusMessage Optional human-readable message describing the current task state. + * @property createdAt ISO 8601 timestamp when the task was created. + * @property lastUpdatedAt ISO 8601 timestamp when the task was last updated. + * @property ttl Actual retention duration from creation in milliseconds, null for unlimited. + * @property pollInterval Suggested polling interval in milliseconds. + * @property meta Optional metadata for this response. + */ +@Serializable +public data class GetTaskResult( + override val taskId: String, + override val status: TaskStatus, + override val statusMessage: String? = null, + override val createdAt: String, + override val lastUpdatedAt: String, + override val ttl: Long?, + override val pollInterval: Long? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : ClientResult, + ServerResult, + TaskFields + +// ============================================================================ +// tasks/result +// ============================================================================ + +/** + * A request to retrieve the result of a completed task. + * + * This request can be sent by either side (both [ClientRequest] and [ServerRequest]). + * + * @property params The parameters specifying which task to retrieve results for. + */ +@Serializable +public data class GetTaskPayloadRequest(override val params: GetTaskPayloadRequestParams) : + ClientRequest, + ServerRequest { + @EncodeDefault + override val method: Method = Method.Defined.TasksResult + + /** + * The task identifier to retrieve results for. + */ + public val taskId: String + get() = params.taskId + + /** + * Metadata for this request. May include a progressToken for out-of-band progress notifications. + */ + public val meta: RequestMeta? + get() = params.meta +} + +/** + * Parameters for a tasks/result request. + * + * @property taskId The task identifier to retrieve results for. + * @property meta Optional metadata for this request. + */ +@Serializable +public data class GetTaskPayloadRequestParams( + val taskId: String, + @SerialName("_meta") + override val meta: RequestMeta? = null, +) : RequestParams + +/** + * The response to a tasks/result request. + * + * The structure matches the result type of the original request. + * For example, a tools/call task would return the [CallToolResult] structure. + * + * @property json the raw JSON object containing the result payload + */ +@JvmInline +@Serializable +public value class GetTaskPayloadResult(public val json: JsonObject) : + ClientResult, + ServerResult { + /** Optional metadata for this response, extracted from the `_meta` field of [json]. */ + override val meta: JsonObject? + get() = json["_meta"]?.takeIf { it is JsonObject } as? JsonObject + + /** + * Retrieves the value associated with the specified key from the result payload. + * + * @param key the key whose corresponding value is to be returned + * @return the [JsonElement] associated with the specified key, or null if the key does not exist + */ + public operator fun get(key: String): JsonElement? = json[key] +} + +// ============================================================================ +// tasks/list +// ============================================================================ + +/** + * A request to retrieve a list of tasks. + * + * This request can be sent by either side (both [ClientRequest] and [ServerRequest]). + * Supports pagination through the cursor parameter. + * + * @property params Optional pagination parameters to control which page of results to return. + */ +@Serializable +public data class ListTasksRequest(override val params: PaginatedRequestParams? = null) : + ClientRequest, + ServerRequest, + PaginatedRequest { + @EncodeDefault + override val method: Method = Method.Defined.TasksList +} + +/** + * The response to a tasks/list request. + * + * @property tasks The list of tasks. + * @property nextCursor An opaque token representing the pagination position after the last returned result. + * If present, there may be more results available. + * @property meta Optional metadata for this response. + */ +@Serializable +public data class ListTasksResult( + val tasks: List, + override val nextCursor: String? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : ClientResult, + ServerResult, + PaginatedResult + +// ============================================================================ +// tasks/cancel +// ============================================================================ + +/** + * A request to cancel a task. + * + * This request can be sent by either side (both [ClientRequest] and [ServerRequest]). + * + * @property params The parameters specifying which task to cancel. + */ +@Serializable +public data class CancelTaskRequest(override val params: CancelTaskRequestParams) : + ClientRequest, + ServerRequest { + @EncodeDefault + override val method: Method = Method.Defined.TasksCancel + + /** + * The task identifier to cancel. + */ + public val taskId: String + get() = params.taskId + + /** + * Metadata for this request. May include a progressToken for out-of-band progress notifications. + */ + public val meta: RequestMeta? + get() = params.meta +} + +/** + * Parameters for a tasks/cancel request. + * + * @property taskId The task identifier to cancel. + * @property meta Optional metadata for this request. + */ +@Serializable +public data class CancelTaskRequestParams( + val taskId: String, + @SerialName("_meta") + override val meta: RequestMeta? = null, +) : RequestParams + +/** + * The response to a tasks/cancel request. + * + * Contains all [Task] fields flattened into the result (intersection of Result and Task). + * + * @property taskId The task identifier. + * @property status Current task state. + * @property statusMessage Optional human-readable message describing the current task state. + * @property createdAt ISO 8601 timestamp when the task was created. + * @property lastUpdatedAt ISO 8601 timestamp when the task was last updated. + * @property ttl Actual retention duration from creation in milliseconds, null for unlimited. + * @property pollInterval Suggested polling interval in milliseconds. + * @property meta Optional metadata for this response. + */ +public typealias CancelTaskResult = GetTaskResult diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.dsl.kt new file mode 100644 index 0000000..a3a9be3 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.dsl.kt @@ -0,0 +1,172 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonObjectBuilder +import kotlinx.serialization.json.buildJsonObject +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * Creates a [CallToolRequest] using a type-safe DSL builder. + * + * ## Required + * - [name][CallToolRequestBuilder.name] - The name of the tool to call + * + * ## Optional + * - [arguments][CallToolRequestBuilder.arguments] - Arguments to pass to the tool + * - [meta][CallToolRequestBuilder.meta] - Metadata for the request + * + * Example without arguments: + * ```kotlin + * val request = buildCallToolRequest { + * name = "getCurrentTime" + * } + * ``` + * + * Example with arguments: + * ```kotlin + * val request = buildCallToolRequest { + * name = "searchDatabase" + * arguments { + * put("query", "users") + * put("limit", 10) + * } + * } + * ``` + * + * @param block Configuration lambda for setting up the call tool request + * @return A configured [CallToolRequest] instance + * @see CallToolRequestBuilder + * @see CallToolRequest + */ +@OptIn(ExperimentalContracts::class) +@ExperimentalMcpApi +public inline fun buildCallToolRequest(block: CallToolRequestBuilder.() -> Unit): CallToolRequest { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + return CallToolRequestBuilder().apply(block).build() +} + +/** + * DSL builder for constructing [CallToolRequest] instances. + * + * This builder invokes a specific tool by name with optional arguments. + * + * ## Required + * - [name] - The name of the tool to call + * + * ## Optional + * - [arguments] - Arguments to pass to the tool + * - [meta] - Metadata for the request + * + * @see buildCallToolRequest + * @see CallToolRequest + */ +@McpDsl +public class CallToolRequestBuilder @PublishedApi internal constructor() : RequestBuilder() { + /** + * The name of the tool to call. This is a required field. + * + * Example: `name = "getCurrentTime"` + */ + public var name: String? = null + + private var arguments: JsonObject? = null + + /** + * Sets tool arguments directly from a JsonObject. + * + * Example: + * ```kotlin + * arguments(buildJsonObject { + * put("query", "users") + * }) + * ``` + */ + public fun arguments(arguments: JsonObject) { + this.arguments = arguments + } + + /** + * Sets tool arguments using a DSL builder. + * + * This is the recommended way to provide tool arguments. + * + * Example: + * ```kotlin + * arguments { + * put("query", "SELECT * FROM users") + * put("limit", 100) + * put("includeDeleted", false) + * } + * ``` + * + * @param block Lambda for building the arguments JsonObject + */ + public fun arguments(block: JsonObjectBuilder.() -> Unit): Unit = arguments(buildJsonObject(block)) + + @PublishedApi + override fun build(): CallToolRequest { + val name = requireNotNull(name) { + "Missing required field 'name'. Example: name = \"toolName\"" + } + + val params = CallToolRequestParams(name = name, arguments = arguments, meta = meta) + return CallToolRequest(params) + } +} + +/** + * Creates a [ListToolsRequest] using a type-safe DSL builder. + * + * ## Optional + * - [cursor][ListToolsRequestBuilder.cursor] - Pagination cursor for fetching next page + * - [meta][ListToolsRequestBuilder.meta] - Metadata for the request + * + * Example without pagination: + * ```kotlin + * val request = buildListToolsRequest { } + * ``` + * + * Example with pagination: + * ```kotlin + * val request = buildListToolsRequest { + * cursor = "eyJwYWdlIjogMn0=" + * } + * ``` + * + * @param block Configuration lambda for setting up the list tools request + * @return A configured [ListToolsRequest] instance + * @see ListToolsRequestBuilder + * @see ListToolsRequest + */ +@OptIn(ExperimentalContracts::class) +@ExperimentalMcpApi +public inline fun buildListToolsRequest(block: ListToolsRequestBuilder.() -> Unit): ListToolsRequest { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + return ListToolsRequestBuilder().apply(block).build() +} + +/** + * DSL builder for constructing [ListToolsRequest] instances. + * + * This builder retrieves a list of available tools, with optional pagination support. + * All fields are optional. + * + * ## Optional + * - [cursor] - Pagination cursor (inherited from [PaginatedRequestBuilder]) + * - [meta] - Metadata for the request (inherited from [RequestBuilder]) + * + * @see buildListToolsRequest + * @see ListToolsRequest + * @see PaginatedRequestBuilder + */ +@McpDsl +public class ListToolsRequestBuilder @PublishedApi internal constructor() : PaginatedRequestBuilder() { + @PublishedApi + override fun build(): ListToolsRequest { + val params = paginatedRequestParams(cursor = cursor, meta = meta) + return ListToolsRequest(params) + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.kt new file mode 100644 index 0000000..1f0f54e --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.kt @@ -0,0 +1,308 @@ +@file:OptIn(ExperimentalSerializationApi::class) + +package io.modelcontextprotocol.kotlin.sdk.types + +import kotlinx.serialization.EncodeDefault +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject + +/** + * Creates a [CallToolResult] with single [TextContent] and [meta]. + */ +public fun CallToolResult.Companion.success(content: String, meta: JsonObject? = null): CallToolResult = CallToolResult( + content = listOf(TextContent(content)), + isError = false, + meta = meta, +) + +/** + * Creates a [CallToolResult] with single [TextContent] and [meta], with `isError` being true. + */ +public fun CallToolResult.Companion.error(content: String, meta: JsonObject? = null): CallToolResult = CallToolResult( + content = listOf(TextContent(content)), + isError = true, + meta = meta, +) + +/** + * Definition for a tool the client can call. + * + * Tools allow servers to expose functionality that can be invoked by clients and LLMs. + * Each tool has a name, description, and schema defining its input (and optionally output) parameters. + * + * **Display name precedence order:** [title] → [annotations].[title] → [name] + * + * @property name The programmatic identifier for this tool. + * Intended for logical use and API identification. Used as a display name fallback + * if both [title] and [annotations].[title] are not provided. + * @property inputSchema A JSON Schema object defining the expected parameters for the tool. + * Must be an object type schema. Defines what arguments the tool accepts. + * Defaults to the JSON Schema 2020-12 dialect when no explicit `$schema` is provided. + * @property description A human-readable description of the tool and when to use it. + * Clients can use this to improve the LLM's understanding of available tools. + * It can be thought of like a "hint" to the model. + * @property outputSchema An optional JSON Schema object defining the structure of the tool's output + * returned in the [structuredContent][CallToolResult.structuredContent] field of a [CallToolResult]. + * Must be an object type schema if provided. + * Defaults to the JSON Schema 2020-12 dialect when no explicit `$schema` is provided. + * @property title Optional human-readable display name for this tool. + * Intended for UI and end-user contexts, optimized to be easily understood + * even by those unfamiliar with domain-specific terminology. + * Note: For Tool specifically, [annotations].[title] takes precedence over this field. + * @property annotations Optional additional tool information providing hints about tool behavior. + * All properties in [ToolAnnotations] are hints and not guaranteed to provide + * a faithful description of tool behavior. + * @property icons Optional set of sized icons that clients can display in their user interface. + * Clients MUST support at least PNG and JPEG formats. + * Clients SHOULD also support SVG and WebP formats. + * @property execution Optional execution-related properties for this tool, + * such as task-augmented execution support. + * @property meta Optional metadata for this tool. + */ +@Serializable +public data class Tool( + val name: String, + val inputSchema: ToolSchema, + val description: String? = null, + val outputSchema: ToolSchema? = null, + val title: String? = null, + val annotations: ToolAnnotations? = null, + val icons: List? = null, + val execution: ToolExecution? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : WithMeta + +/** + * A JSON Schema for tool input or output parameters. + * + * This is a simplified schema structure that must be of type "object". + * Defaults to the JSON Schema 2020-12 dialect when no explicit [schema] (`$schema`) is provided. + * + * @property schema Optional URI identifying the JSON Schema dialect (e.g., + * `https://json-schema.org/draft/2020-12/schema`). Serialized as `$schema`. + * When absent, JSON Schema 2020-12 is assumed by default. + * @property properties Optional map of property names to their schema definitions. + * @property required Optional list of property names that are required. + * @property defs Optional schema definitions available to references in [properties]. Serialized as `$defs`. + */ +@Serializable +public data class ToolSchema( + @SerialName("\$schema") + val schema: String? = null, + val properties: JsonObject? = null, + val required: List? = null, + @SerialName("\$defs") + val defs: JsonObject? = null, +) { + /** Always `"object"` for tool schemas. */ + @OptIn(ExperimentalSerializationApi::class) + @EncodeDefault + val type: String = "object" +} + +/** + * Execution-related properties for a tool. + * + * @property taskSupport Indicates whether this tool supports task-augmented execution. + * This allows clients to handle long-running operations through polling the task system. + * If omitted (`null`), consumers should treat it as [TaskSupport.Forbidden] per the spec. + */ +@Serializable +public data class ToolExecution(val taskSupport: TaskSupport? = null) + +/** + * Indicates whether a tool supports task-augmented execution. + * + * - [Forbidden]: Tool does not support task-augmented execution (default when absent). + * - [Optional]: Tool may support task-augmented execution. + * - [Required]: Tool requires task-augmented execution. + */ +@Serializable +public enum class TaskSupport { + /** Tool does not support task-augmented execution. */ + @SerialName("forbidden") + Forbidden, + + /** Tool may support task-augmented execution. */ + @SerialName("optional") + Optional, + + /** Tool requires task-augmented execution. */ + @SerialName("required") + Required, +} + +/** + * Additional properties describing a Tool to clients. + * + * **IMPORTANT:** All properties in ToolAnnotations are **hints**. They are NOT guaranteed to provide + * a faithful description of tool behavior (including descriptive properties like [title]). + * + * **Security warning:** Clients should NEVER make tool use decisions based on ToolAnnotations + * received from untrusted servers. + * + * @property title A human-readable title for the tool. + * Takes precedence over [Tool.title] and [Tool.name] for display purposes. + * @property readOnlyHint If true, the tool does not modify its environment. + * If false, the tool may perform modifications. + * Default: false + * @property destructiveHint If true, the tool may perform destructive updates to its environment. + * If false, the tool performs only additive updates. + * This property is meaningful only when [readOnlyHint] == false. + * Default: true + * @property idempotentHint If true, calling the tool repeatedly with the same arguments will have + * no additional effect on its environment. + * This property is meaningful only when [readOnlyHint] == false. + * Default: false + * @property openWorldHint If true, this tool may interact with an "open world" of external entities. + * If false, the tool's domain of interaction is closed. + * For example, the world of a web search tool is open, whereas that of a + * memory tool is not. + * Default: true + */ +@Serializable +public data class ToolAnnotations( + val title: String? = null, + val readOnlyHint: Boolean? = null, + val destructiveHint: Boolean? = null, + val idempotentHint: Boolean? = null, + val openWorldHint: Boolean? = null, +) + +// ============================================================================ +// tools/call +// ============================================================================ + +/** + * Used by the client to invoke a tool provided by the server. + * + * Tools allow servers to expose functionality that clients (and LLMs) can call. + * Examples include file operations, API calls, database queries, or any other + * server-side operations. + * + * @property params The parameters specifying which tool to call and what arguments to pass. + */ +@Serializable +public data class CallToolRequest(override val params: CallToolRequestParams) : ClientRequest { + @EncodeDefault + override val method: Method = Method.Defined.ToolsCall + + /** + * The name of the tool to invoke. + */ + public val name: String + get() = params.name + + /** + * Arguments to pass to the tool. Keys are argument names, values are the argument values. + * The structure must match the tool's input schema. + */ + public val arguments: JsonObject? + get() = params.arguments + + /** + * Task metadata, if the caller is requesting task-augmented execution. + */ + public val task: TaskMetadata? + get() = params.task + + /** + * Metadata for this request. May include a progressToken for out-of-band progress notifications. + */ + public val meta: RequestMeta? + get() = params.meta +} + +/** + * Parameters for a tools/call request. + * + * @property name The name of the tool to invoke. + * @property arguments Arguments to pass to the tool. Keys are argument names, values are the argument values. + * The structure must match the tool's input schema. + * @property task If set, requests task-augmented execution. A receiver that supports it returns + * a [CreateTaskResult] (result via `tasks/result`); otherwise it returns a [CallToolResult] directly. + * @property meta Optional metadata for this request. May include a progressToken for + * out-of-band progress notifications. + */ +@Serializable +public data class CallToolRequestParams( + val name: String, + val arguments: JsonObject? = null, + val task: TaskMetadata? = null, + @SerialName("_meta") + override val meta: RequestMeta? = null, +) : RequestParams + +/** + * The server's response to a [CallToolRequest]. + * + * Contains the result of the tool execution, which can be successful or an error. + * + * **Important error handling:** + * - Errors that originate from the **tool itself** SHOULD be reported inside the result object + * with [isError] set to true, NOT as an MCP protocol-level error response. This allows the LLM + * to see that an error occurred and potentially self-correct. + * - Errors in **finding the tool**, unsupported operations, or other exceptional conditions + * SHOULD be reported as MCP protocol-level error responses. + * + * @property content A list of content blocks that represent the unstructured result of the tool call. + * This is what the LLM will see as the tool's output. + * @property isError Whether the tool call ended in an error. + * If not set, this is assumed to be false (the call was successful). + * When true, the [content] should describe the error that occurred. + * @property structuredContent An optional JSON object that represents the structured result of the tool call. + * Provides machine-readable output in addition to the human-readable [content]. + * @property meta Optional metadata for this response. + */ +@Serializable +public data class CallToolResult( + val content: List, + val isError: Boolean? = null, + val structuredContent: JsonObject? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : ServerResult + +// ============================================================================ +// tools/list +// ============================================================================ + +/** + * Sent from the client to request a list of tools the server has. + * + * This request supports pagination through the cursor parameter. If the server has many + * tools, it may return a subset along with a cursor to fetch the next page. + * + * @property params Optional pagination parameters to control which page of results to return. + */ +@Serializable +public data class ListToolsRequest(override val params: PaginatedRequestParams? = null) : + ClientRequest, + PaginatedRequest { + @EncodeDefault + override val method: Method = Method.Defined.ToolsList +} + +/** + * The server's response to a [ListToolsRequest] from the client. + * + * Returns the available tools along with pagination information if there are more results. + * + * @property tools The list of available tools. Each tool includes its name, description, + * and input schema that defines what arguments it accepts. + * @property nextCursor An opaque token representing the pagination position after the last returned result. + * If present, there may be more results available. The client can pass this token + * in a subsequent request to fetch the next page. + * @property meta Optional metadata for this response. + */ +@Serializable +public data class ListToolsResult( + val tools: List, + override val nextCursor: String? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : ServerResult, + PaginatedResult diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcher.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcher.kt new file mode 100644 index 0000000..aa2abb9 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcher.kt @@ -0,0 +1,154 @@ +package io.modelcontextprotocol.kotlin.sdk.utils + +import io.github.oshai.kotlinlogging.KotlinLogging +import io.ktor.http.decodeURLPart +import io.modelcontextprotocol.kotlin.sdk.types.ResourceTemplate +import kotlinx.collections.immutable.toImmutableMap +import kotlin.jvm.JvmOverloads +import kotlin.jvm.JvmStatic + +// Max URL/path length to prevent DoS or unexpected large payloads. +private const val MAX_URL_LENGTH = 2048 + +// Max template/uri depth to prevent overly nested or complex templates. +private const val MAX_DEPTH = 50 + +// Literal segments are more specific than variable captures. +private const val LITERAL_MATCH_SCORE = 2 +private const val VARIABLE_MATCH_SCORE = 1 + +/** + * A [ResourceTemplateMatcher] that matches resource URIs against an RFC 6570 Level 1 + * URI template by splitting both the URI and the template on `/` and comparing each + * segment in order. + * + * ### Supported template syntax + * + * Only RFC 6570 **Level 1** is supported: simple `{variable}` expressions where the + * entire path segment is a single variable. Operator expressions (`{+var}`, `{#var}`, + * `{.var}`, `{/var}`, etc.) and multi-variable expressions (`{a,b}`) are **not** + * recognized — segments containing them are treated as literals. + * + * ### Matching rules + * + * - The URI and template must have the same number of `/`-delimited segments. + * - Literal segments must match exactly (after percent-decoding the URI segment). + * - `{variable}` segments capture the percent-decoded URI segment value. + * - Query strings and fragments (`?`, `#`) are **not** stripped — they become part of + * the captured variable value for the segment that contains them. + * + * ### Specificity scoring + * + * When multiple templates match the same URI, each matched literal segment contributes + * 2 points and each variable capture contributes 1 point. The highest-scoring match wins. + * + * ### Safety limits + * + * - URIs longer than [maxUriLength] characters are rejected. + * - Templates and URIs with more than [maxDepth] segments are rejected. + * + * ### Security contract for handler authors + * + * Values in [MatchResult.variables] are attacker-controlled strings extracted from + * the incoming URI. They are percent-decoded (one pass only) before being returned. + * Handlers **must** treat them as untrusted input and validate or sanitize them + * before using them to construct file paths, database queries, downstream URLs, or + * any other security-sensitive operation. + * + * In particular: + * - A decoded value may contain `/`, `..`, null bytes (`\u0000`), `?`, or `#`. + * - `%252F` in the URI becomes `%2F` in the variable — exactly one decode pass is applied. + * + * ### Platform limitations + * + * Dot-segment normalization (resolving `..` and `.` in the URI path) is performed + * on JVM and JS targets using the platform-native URI parser. On native and WASM targets + * no normalizer is available, so dot-segment traversal is **not** mitigated at this + * layer — handlers on those targets must normalize paths themselves. + * + * @property resourceTemplate the resource template to match against; must follow RFC 6570 Level 1 syntax + * @param maxDepth maximum allowed segment count for the template/uri (defaults to 50) + * @param maxUriLength maximum allowed length for incoming URIs (defaults to 2048) + */ +public class PathSegmentTemplateMatcher @JvmOverloads constructor( + override val resourceTemplate: ResourceTemplate, + private val maxDepth: Int = MAX_DEPTH, + private val maxUriLength: Int = MAX_URL_LENGTH, +) : ResourceTemplateMatcher { + + public companion object { + /** + * A [ResourceTemplateMatcherFactory] that creates [PathSegmentTemplateMatcher] instances + * with default limits. Pass this to [io.modelcontextprotocol.kotlin.sdk.server.ServerOptions] + * to use path-segment matching, or supply a custom factory to override the matching strategy. + */ + @JvmStatic + public val factory: ResourceTemplateMatcherFactory = ResourceTemplateMatcherFactory { + PathSegmentTemplateMatcher(it) + } + + @JvmStatic + private val logger = KotlinLogging.logger {} + } + + private val templateParts: List + + // Maps segment index to variable name; indices absent from this map are literal segments. + private val variableIndices: Map + + init { + val template = resourceTemplate.uriTemplate + require(template.isNotBlank()) { "Resource template cannot be blank" } + templateParts = template.trim('/').split("/") + require(templateParts.size <= maxDepth) { + "Template is too complex (max depth=$maxDepth)" + } + + val vars = mutableMapOf() + for (i in templateParts.indices) { + val segment = templateParts[i] + if (segment.startsWith("{") && segment.endsWith("}")) { + val name = segment.removeSurrounding("{", "}").trim() + require(name.isNotEmpty()) { "Invalid variable name in template: $segment" } + vars[i] = name + } + } + variableIndices = vars.toImmutableMap() + } + + override fun match(resourceUri: String): MatchResult? { + if (resourceUri.length > maxUriLength) { + logger.debug { "URL is too long (max=$maxUriLength)" } + return null + } + + // Resolve dot-segments (e.g. /a/../b → /a/b) before splitting. + // Prevents traversal attacks on JVM/JS; no-op on native/WASM targets. + val normalized = normalizeUri(resourceUri) + + val urlParts = normalized.trim('/').split("/") + if (urlParts.size > maxDepth) { + logger.debug { "URI has too many segments (max=$maxDepth)" } + return null + } + if (urlParts.size != templateParts.size) return null + + val variables = mutableMapOf() + var score = 0 + + for (i in templateParts.indices) { + val urlSegment = urlParts[i].decodeURLPart() + val variableName = variableIndices[i] + if (variableName != null) { + variables[variableName] = urlSegment + score += VARIABLE_MATCH_SCORE + } else if (templateParts[i] == urlSegment) { + score += LITERAL_MATCH_SCORE + } else { + return null + } + } + + return MatchResult(variables = variables.toImmutableMap(), score = score) + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcher.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcher.kt new file mode 100644 index 0000000..d99bfda --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcher.kt @@ -0,0 +1,68 @@ +package io.modelcontextprotocol.kotlin.sdk.utils + +import io.modelcontextprotocol.kotlin.sdk.types.ResourceTemplate + +/** + * Represents the result of a successful template match. + * + * A higher [score] indicates a more specific match. Implementations must ensure that + * literal segment matches contribute more to the score than variable captures, so that + * a fully literal template (e.g., `users/profile`) always outscores a parameterized + * template (e.g., `users/{id}`) for the same URI. + * + * @property variables A mapping of variable names in the template to their matched values in the URL. + * @property score A non-negative measure of match specificity — higher means more literal segments matched. + */ +public class MatchResult(public val variables: Map, public val score: Int) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is MatchResult) return false + return score == other.score && variables == other.variables + } + + override fun hashCode(): Int = 31 * variables.hashCode() + score + + override fun toString(): String = "MatchResult(variables=$variables, score=$score)" +} + +/** + * Matches resource URIs against a [ResourceTemplate]. + * + * Implementations parse a URI template once at construction time and then match + * candidate URIs against that template via [match]. The returned [MatchResult.score] + * must reflect match specificity so that a selection algorithm can prefer the most + * specific template when multiple templates match the same URI. + */ +public interface ResourceTemplateMatcher { + + /** The template to match URIs against. */ + public val resourceTemplate: ResourceTemplate + + /** + * Matches a given resource URI against the defined resource template. + * + * @param resourceUri The resource URI to be matched. + * @return A [MatchResult] containing the mapping of variables and a match score + * if the URI matches the template, or null if no match is found. + */ + public fun match(resourceUri: String): MatchResult? +} + +/** + * Factory interface for creating instances of [ResourceTemplateMatcher]. + * + * A [ResourceTemplateMatcher] is used to match resource URIs against a given + * [ResourceTemplate], which adheres to the RFC 6570 URI Template specification. + * This factory abstracts the creation process of a matcher, allowing different + * implementations to define custom matching logic or safeguards (e.g., security + * measures or restrictions on template complexity). + */ +public fun interface ResourceTemplateMatcherFactory { + /** + * Creates a resource template matcher for the given resource template. + * + * @param resourceTemplate The resource template to create a matcher for. + * @return A [ResourceTemplateMatcher] instance that can match URIs against the provided template. + */ + public fun create(resourceTemplate: ResourceTemplate): ResourceTemplateMatcher +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.kt new file mode 100644 index 0000000..452333b --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.kt @@ -0,0 +1,16 @@ +package io.modelcontextprotocol.kotlin.sdk.utils + +/** + * Normalizes [uri] by resolving dot-segments (`/a/b/../c` → `/a/c`) using the + * platform-native URI parser. + * + * On JVM uses [java.net.URI.normalize]. On JS uses the browser/Node.js `URL` API. + * On native and WASM targets [uri] is returned unchanged — no platform normalizer + * is available, so callers on those targets do not receive dot-segment resolution. + * + * Returns [uri] unchanged if it cannot be parsed or normalized. + * + * **Security note**: call this before splitting a URI into segments to prevent + * dot-segment traversal attacks (e.g. `public/../private/secret`). + */ +internal expect fun normalizeUri(uri: String): String diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/AudioContentSerializationTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/AudioContentSerializationTest.kt deleted file mode 100644 index 247388f..0000000 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/AudioContentSerializationTest.kt +++ /dev/null @@ -1,33 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk - -import io.kotest.assertions.json.shouldEqualJson -import io.modelcontextprotocol.kotlin.sdk.shared.McpJson -import kotlin.test.Test -import kotlin.test.assertEquals - -class AudioContentSerializationTest { - - private val audioContentJson = """ - { - "data": "base64-encoded-audio-data", - "mimeType": "audio/wav", - "type": "audio" - } - """.trimIndent() - - private val audioContent = AudioContent( - data = "base64-encoded-audio-data", - mimeType = "audio/wav", - ) - - @Test - fun `should serialize audio content`() { - McpJson.encodeToString(audioContent) shouldEqualJson audioContentJson - } - - @Test - fun `should deserialize audio content`() { - val content = McpJson.decodeFromString(audioContentJson) - assertEquals(expected = audioContent, actual = content) - } -} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/CallToolResultUtilsTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/CallToolResultUtilsTest.kt deleted file mode 100644 index d331b01..0000000 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/CallToolResultUtilsTest.kt +++ /dev/null @@ -1,63 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk - -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.buildJsonObject -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -class CallToolResultUtilsTest { - - @Test - fun testOkWithOnlyText() { - val content = "TextMessage" - val result = CallToolResult.Companion.ok(content) - - assertEquals(1, result.content.size) - assertEquals(content, (result.content[0] as TextContent).text) - assertFalse(result.isError == true) - assertEquals(EmptyJsonObject, result._meta) - } - - @Test - fun testOkWithMeta() { - val content = "TextMessageWithMeta" - val meta = buildJsonObject { - put("key1", JsonPrimitive("value1")) - put("key2", JsonPrimitive(42)) - } - val result = CallToolResult.Companion.ok(content, meta) - - assertEquals(1, result.content.size) - assertEquals(content, (result.content[0] as TextContent).text) - assertFalse(result.isError == true) - assertEquals(meta, result._meta) - } - - @Test - fun testErrorWithOnlyText() { - val content = "ErrorMessage" - val result = CallToolResult.Companion.error(content) - - assertEquals(1, result.content.size) - assertEquals(content, (result.content[0] as TextContent).text) - assertTrue(result.isError == true) - assertEquals(EmptyJsonObject, result._meta) - } - - @Test - fun testErrorWithMeta() { - val content = "ErrorMessageWithMeta" - val meta = buildJsonObject { - put("errorCode", JsonPrimitive(404)) - put("errorDetail", JsonPrimitive("资源未找到")) - } - val result = CallToolResult.Companion.error(content, meta) - - assertEquals(1, result.content.size) - assertEquals(content, (result.content[0] as TextContent).text) - assertTrue(result.isError == true) - assertEquals(meta, result._meta) - } -} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/RequestSerializerTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/RequestSerializerTest.kt deleted file mode 100644 index 14f6779..0000000 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/RequestSerializerTest.kt +++ /dev/null @@ -1,324 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk - -import io.modelcontextprotocol.kotlin.sdk.shared.McpJson -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.put -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertIs - -class RequestSerializerTest { - - // Client Result Tests - @Test - fun `should deserialize CreateMessageResult polymorphically`() { - val json = """{ - "model": "test-model", - "role": "assistant", - "content": { - "type": "text", - "text": "Hello" - }, - "stopReason": "endTurn" - }""" - - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals("test-model", decoded.model) - assertEquals(Role.assistant, decoded.role) - assertEquals(StopReason.EndTurn, decoded.stopReason) - } - - @Test - fun `should deserialize ListRootsResult polymorphically`() { - val json = """{ - "roots": [ - { - "uri": "file:///test", - "name": "Test Root" - } - ] - }""" - - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals(1, decoded.roots.size) - assertEquals("file:///test", decoded.roots[0].uri) - assertEquals("Test Root", decoded.roots[0].name) - } - - @Test - fun `should deserialize CreateElicitationResult polymorphically`() { - val json = """{ - "action": "accept", - "content": { - "timezone": "Europe/Amsterdam" - } - }""" - - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals(CreateElicitationResult.Action.accept, decoded.action) - } - - // Server Result Tests - @Test - fun `should deserialize ListToolsResult polymorphically`() { - val json = """{ - "tools": [ - { - "name": "test-tool", - "description": "A test tool", - "inputSchema": { - "type": "object", - "properties": {} - } - } - ] - }""" - - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals(1, decoded.tools.size) - assertEquals("test-tool", decoded.tools[0].name) - assertEquals("A test tool", decoded.tools[0].description) - } - - @Test - fun `should deserialize ListResourcesResult polymorphically`() { - val json = """{ - "resources": [ - { - "uri": "file:///test.txt", - "name": "test.txt", - "mimeType": "text/plain" - } - ] - }""" - - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals(1, decoded.resources.size) - assertEquals("file:///test.txt", decoded.resources[0].uri) - assertEquals("test.txt", decoded.resources[0].name) - assertEquals("text/plain", decoded.resources[0].mimeType) - } - - @Test - fun `should deserialize ListResourceTemplatesResult polymorphically`() { - val json = """{ - "resourceTemplates": [ - { - "uriTemplate": "file:///templates/{name}", - "name": "template", - "mimeType": "text/plain" - } - ] - }""" - - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals(1, decoded.resourceTemplates.size) - assertEquals("file:///templates/{name}", decoded.resourceTemplates[0].uriTemplate) - assertEquals("template", decoded.resourceTemplates[0].name) - assertEquals("text/plain", decoded.resourceTemplates[0].mimeType) - } - - @Test - fun `should deserialize ListPromptsResult polymorphically`() { - val json = """{ - "prompts": [ - { - "name": "test-prompt", - "description": "A test prompt" - } - ] - }""" - - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals(1, decoded.prompts.size) - assertEquals("test-prompt", decoded.prompts[0].name) - assertEquals("A test prompt", decoded.prompts[0].description) - } - - @Test - fun `should deserialize InitializeResult polymorphically`() { - val json = """{ - "capabilities": { - "logging": {}, - "prompts": { - "listChanged": true - }, - "resources": { - "subscribe": true, - "listChanged": true - }, - "tools": { - "listChanged": true - } - }, - "protocolVersion": "2024-11-05", - "serverInfo": { - "name": "Test Server", - "version": "1.0.0" - } - }""" - - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals("2024-11-05", decoded.protocolVersion) - assertEquals("Test Server", decoded.serverInfo.name) - assertEquals("1.0.0", decoded.serverInfo.version) - } - - @Test - fun `should deserialize GetPromptResult polymorphically`() { - val json = """{ - "description": "A test prompt", - "messages": [ - { - "role": "user", - "content": { - "type": "text", - "text": "Hello" - } - } - ] - }""" - - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals("A test prompt", decoded.description) - assertEquals(1, decoded.messages.size) - assertEquals(Role.user, decoded.messages[0].role) - } - - @Test - fun `should deserialize CompleteResult polymorphically`() { - val json = """{ - "completion": { - "values": ["option1", "option2"], - "total": 2, - "hasMore": false - } - }""" - - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals(2, decoded.completion.values.size) - assertEquals("option1", decoded.completion.values[0]) - assertEquals("option2", decoded.completion.values[1]) - assertEquals(2, decoded.completion.total) - assertEquals(false, decoded.completion.hasMore) - } - - @Test - fun `should deserialize ReadResourceResult polymorphically`() { - val json = """{ - "contents": [ - { - "uri": "file:///test.txt", - "mimeType": "text/plain", - "text": "Hello World" - } - ] - }""" - - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals(1, decoded.contents.size) - assertIs(decoded.contents[0]) - val textContent = decoded.contents[0] as TextResourceContents - assertEquals("file:///test.txt", textContent.uri) - assertEquals("text/plain", textContent.mimeType) - assertEquals("Hello World", textContent.text) - } - - @Test - fun `should deserialize CallToolResult polymorphically`() { - val json = """{ - "content": [ - { - "type": "text", - "text": "Tool result" - } - ], - "isError": false - }""" - - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals(1, decoded.content.size) - assertIs(decoded.content[0]) - assertEquals("Tool result", (decoded.content[0] as TextContent).text) - assertEquals(false, decoded.isError) - } - - @Test - fun `should deserialize CompatibilityCallToolResult polymorphically`() { - val json = """{ - "toolResult": {"result": "Legacy tool result"}, - "content": [], - "isError": false - }""" - - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals(buildJsonObject { put("result", "Legacy tool result") }, decoded.toolResult) - } - - // Fallback Test - @Test - fun `should deserialize EmptyRequestResult for unknown result type`() { - val json = """{"unknownField": "value"}""" - - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals(EmptyJsonObject, decoded._meta) - } - - @Test - fun `should handle empty JSON object`() { - val json = """{}""" - - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - } - - // Priority Test - Client results should take precedence over server results - @Test - fun `should prioritize client results over server results when both match`() { - // This JSON could potentially match both CreateMessageResult (client) and CallToolResult (server) - // but CreateMessageResult should be selected first due to the order - val json = """{ - "model": "test-model", - "role": "assistant", - "content": { - "type": "text", - "text": "Test message" - }, - "stopReason": "endTurn" - }""" - - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals("test-model", decoded.model) - } -} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/ToolSerializationTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/ToolSerializationTest.kt deleted file mode 100644 index 0664351..0000000 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/ToolSerializationTest.kt +++ /dev/null @@ -1,488 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk - -import io.kotest.assertions.json.shouldEqualJson -import io.modelcontextprotocol.kotlin.sdk.shared.McpJson -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.buildJsonObject -import kotlin.test.Test -import kotlin.test.assertEquals - -class ToolSerializationTest { - - // see https://docs.anthropic.com/en/docs/build-with-claude/tool-use - /* language=json */ - private val getWeatherToolJson = """ - { - "name": "get_weather", - "title": "Get weather", - "description": "Get the current weather in a given location", - "inputSchema": { - "type": "object", - "properties": { - "location": { - "type": "string", - "description": "The city and state, e.g. San Francisco, CA" - } - }, - "required": ["location"] - }, - "outputSchema": { - "type": "object", - "properties": { - "temperature": { - "type": "number", - "description": "Temperature in celsius" - }, - "conditions": { - "type": "string", - "description": "Weather conditions description" - }, - "humidity": { - "type": "number", - "description": "Humidity percentage" - } - }, - "required": ["temperature", "conditions", "humidity"] - } - } - """.trimIndent() - - val getWeatherTool = Tool( - name = "get_weather", - title = "Get weather", - description = "Get the current weather in a given location", - annotations = null, - inputSchema = Tool.Input( - properties = buildJsonObject { - put( - "location", - buildJsonObject { - put("type", JsonPrimitive("string")) - put("description", JsonPrimitive("The city and state, e.g. San Francisco, CA")) - }, - ) - }, - required = listOf("location"), - ), - outputSchema = Tool.Output( - properties = buildJsonObject { - put( - "temperature", - buildJsonObject { - put("type", JsonPrimitive("number")) - put("description", JsonPrimitive("Temperature in celsius")) - }, - ) - put( - "conditions", - buildJsonObject { - put("type", JsonPrimitive("string")) - put("description", JsonPrimitive("Weather conditions description")) - }, - ) - put( - "humidity", - buildJsonObject { - put("type", JsonPrimitive("number")) - put("description", JsonPrimitive("Humidity percentage")) - }, - ) - }, - required = listOf("temperature", "conditions", "humidity"), - ), - ) - - //region Serialize - - @Test - fun `should serialize get_weather tool`() { - McpJson.encodeToString(getWeatherTool) shouldEqualJson getWeatherToolJson - } - - @Test - fun `should always serialize default value`() { - val json = Json(from = McpJson) { - encodeDefaults = false - } - json.encodeToString(getWeatherTool) shouldEqualJson getWeatherToolJson - } - - @Test - fun `should serialize get_weather tool without optional properties`() { - val weatherTool = createWeatherTool(name = "get_weather") - val expectedJson = createWeatherToolJson(name = "get_weather") - val actualJson = McpJson.encodeToString(weatherTool) - - actualJson shouldEqualJson expectedJson - } - - @Test - fun `should serialize get_weather tool with title optional property specified`() { - val weatherTool = createWeatherTool(name = "get_weather", title = "Get weather") - val expectedJson = createWeatherToolJson(name = "get_weather", title = "Get weather") - val actualJson = McpJson.encodeToString(weatherTool) - - actualJson shouldEqualJson expectedJson - } - - @Test - fun `should serialize get_weather tool with outputSchema optional property specified`() { - val weatherTool = createWeatherTool( - name = "get_weather", - outputSchema = Tool.Output( - properties = buildJsonObject { - put( - "temperature", - buildJsonObject { - put("type", JsonPrimitive("number")) - put("description", JsonPrimitive("Temperature in celsius")) - }, - ) - put( - "conditions", - buildJsonObject { - put("type", JsonPrimitive("string")) - put("description", JsonPrimitive("Weather conditions description")) - }, - ) - put( - "humidity", - buildJsonObject { - put("type", JsonPrimitive("number")) - put("description", JsonPrimitive("Humidity percentage")) - }, - ) - }, - required = listOf("temperature", "conditions", "humidity"), - ), - ) - val expectedJson = - createWeatherToolJson( - name = "get_weather", - outputSchema = """ - { - "type": "object", - "properties": { - "temperature": { - "type": "number", - "description": "Temperature in celsius" - }, - "conditions": { - "type": "string", - "description": "Weather conditions description" - }, - "humidity": { - "type": "number", - "description": "Humidity percentage" - } - }, - "required": ["temperature", "conditions", "humidity"] - } - """.trimIndent(), - ) - - val actualJson = McpJson.encodeToString(weatherTool) - - actualJson shouldEqualJson expectedJson - } - - @Test - fun `should serialize get_weather tool with all properties specified`() { - val weatherTool = createWeatherTool( - name = "get_weather", - title = "Get weather", - outputSchema = Tool.Output( - properties = buildJsonObject { - put( - "temperature", - buildJsonObject { - put("type", JsonPrimitive("number")) - put("description", JsonPrimitive("Temperature in celsius")) - }, - ) - put( - "conditions", - buildJsonObject { - put("type", JsonPrimitive("string")) - put("description", JsonPrimitive("Weather conditions description")) - }, - ) - put( - "humidity", - buildJsonObject { - put("type", JsonPrimitive("number")) - put("description", JsonPrimitive("Humidity percentage")) - }, - ) - }, - required = listOf("temperature", "conditions", "humidity"), - ), - ) - val expectedJson = createWeatherToolJson( - name = "get_weather", - title = "Get weather", - outputSchema = """ - { - "type": "object", - "properties": { - "temperature": { - "type": "number", - "description": "Temperature in celsius" - }, - "conditions": { - "type": "string", - "description": "Weather conditions description" - }, - "humidity": { - "type": "number", - "description": "Humidity percentage" - } - }, - "required": ["temperature", "conditions", "humidity"] - } - """.trimIndent(), - ) - - val actualJson = McpJson.encodeToString(weatherTool) - - actualJson shouldEqualJson expectedJson - } - - //endregion Serialize - - //region Deserialize - - @Test - fun `should deserialize get_weather tool`() { - val actualTool = McpJson.decodeFromString(getWeatherToolJson) - assertEquals(expected = getWeatherTool, actual = actualTool) - } - - @Test - fun `should deserialize get_weather tool without optional properties`() { - val toolJson = createWeatherToolJson(name = "get_weather") - val expectedTool = createWeatherTool(name = "get_weather") - val actualTool = McpJson.decodeFromString(toolJson) - - assertEquals(expected = expectedTool, actual = actualTool) - } - - @Test - fun `should deserialize get_weather tool with title properties specified`() { - val toolJson = createWeatherToolJson(name = "get_weather", title = "Get weather") - val expectedTool = createWeatherTool(name = "get_weather", title = "Get weather") - - val actualTool = McpJson.decodeFromString(toolJson) - - assertEquals(expected = expectedTool, actual = actualTool) - } - - @Test - fun `should deserialize get_weather tool with outputSchema optional property specified`() { - val toolJson = - createWeatherToolJson( - name = "get_weather", - outputSchema = """ - { - "type": "object", - "properties": { - "temperature": { - "type": "number", - "description": "Temperature in celsius" - }, - "conditions": { - "type": "string", - "description": "Weather conditions description" - }, - "humidity": { - "type": "number", - "description": "Humidity percentage" - } - }, - "required": ["temperature", "conditions", "humidity"] - } - """.trimIndent(), - ) - - val expectedTool = createWeatherTool( - name = "get_weather", - outputSchema = Tool.Output( - properties = buildJsonObject { - put( - "temperature", - buildJsonObject { - put("type", JsonPrimitive("number")) - put("description", JsonPrimitive("Temperature in celsius")) - }, - ) - put( - "conditions", - buildJsonObject { - put("type", JsonPrimitive("string")) - put("description", JsonPrimitive("Weather conditions description")) - }, - ) - put( - "humidity", - buildJsonObject { - put("type", JsonPrimitive("number")) - put("description", JsonPrimitive("Humidity percentage")) - }, - ) - }, - required = listOf("temperature", "conditions", "humidity"), - ), - ) - - val actualTool = McpJson.decodeFromString(toolJson) - - assertEquals(expected = expectedTool, actual = actualTool) - } - - @Test - fun `should deserialize get_weather tool with all properties specified`() { - val toolJson = createWeatherToolJson( - name = "get_weather", - title = "Get weather", - outputSchema = """ - { - "type": "object", - "properties": { - "temperature": { - "type": "number", - "description": "Temperature in celsius" - }, - "conditions": { - "type": "string", - "description": "Weather conditions description" - }, - "humidity": { - "type": "number", - "description": "Humidity percentage" - } - }, - "required": ["temperature", "conditions", "humidity"] - } - """.trimIndent(), - ) - - val expectedTool = createWeatherTool( - name = "get_weather", - title = "Get weather", - outputSchema = Tool.Output( - properties = buildJsonObject { - put( - "temperature", - buildJsonObject { - put("type", JsonPrimitive("number")) - put("description", JsonPrimitive("Temperature in celsius")) - }, - ) - put( - "conditions", - buildJsonObject { - put("type", JsonPrimitive("string")) - put("description", JsonPrimitive("Weather conditions description")) - }, - ) - put( - "humidity", - buildJsonObject { - put("type", JsonPrimitive("number")) - put("description", JsonPrimitive("Humidity percentage")) - }, - ) - }, - required = listOf("temperature", "conditions", "humidity"), - ), - ) - - val actualTool = McpJson.decodeFromString(toolJson) - - assertEquals(expected = expectedTool, actual = actualTool) - } - - //endregion Deserialize - - //region Private Methods - - private fun createWeatherToolJson( - name: String = "get_weather", - title: String? = null, - outputSchema: String? = null, - ): String { - val stringBuilder = StringBuilder() - - stringBuilder - .appendLine("{") - .append(" \"name\": \"$name\"") - - if (title != null) { - stringBuilder - .appendLine(",") - .append(" \"title\": \"$title\"") - } - - stringBuilder - .appendLine(",") - .append(" \"description\": \"Get the current weather in a given location\"") - .appendLine(",") - .append( - """ - "inputSchema": { - "type": "object", - "properties": { - "location": { - "type": "string", - "description": "The city and state, e.g. San Francisco, CA" - } - }, - "required": ["location"] - } - """.trimIndent(), - ) - - if (outputSchema != null) { - stringBuilder - .appendLine(",") - .append( - """ - "outputSchema": $outputSchema - """.trimIndent(), - ) - } - - stringBuilder - .appendLine() - .appendLine("}") - - return stringBuilder.toString().trimIndent() - } - - private fun createWeatherTool( - name: String = "get_weather", - title: String? = null, - outputSchema: Tool.Output? = null, - ): Tool = Tool( - name = name, - title = title, - description = "Get the current weather in a given location", - annotations = null, - inputSchema = Tool.Input( - properties = buildJsonObject { - put( - "location", - buildJsonObject { - put("type", JsonPrimitive("string")) - put("description", JsonPrimitive("The city and state, e.g. San Francisco, CA")) - }, - ) - }, - required = listOf("location"), - ), - outputSchema = outputSchema, - ) - - //endregion Private Methods -} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesTest.kt deleted file mode 100644 index 23e2ffb..0000000 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesTest.kt +++ /dev/null @@ -1,397 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk - -import io.modelcontextprotocol.kotlin.sdk.shared.McpJson -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertIs -import kotlin.test.assertNotEquals -import kotlin.test.assertTrue -import kotlin.time.ExperimentalTime -import kotlin.time.Instant - -class TypesTest { - - @Test - fun `should have correct latest protocol version`() { - assertNotEquals("", LATEST_PROTOCOL_VERSION) - assertEquals("2025-03-26", LATEST_PROTOCOL_VERSION) - } - - @Test - fun `should have correct supported protocol versions`() { - assertIs>(SUPPORTED_PROTOCOL_VERSIONS) - assertTrue(SUPPORTED_PROTOCOL_VERSIONS.contains(LATEST_PROTOCOL_VERSION)) - assertTrue(SUPPORTED_PROTOCOL_VERSIONS.contains("2024-11-05")) - assertEquals(2, SUPPORTED_PROTOCOL_VERSIONS.size) - } - - @Test - fun `should validate JSONRPC version constant`() { - assertEquals("2.0", JSONRPC_VERSION) - } - - // Reference Tests - @Test - fun `should validate ResourceTemplateReference`() { - val resourceRef = ResourceTemplateReference(uri = "file:///path/to/file.txt") - - assertEquals("ref/resource", resourceRef.type) - assertEquals("file:///path/to/file.txt", resourceRef.uri) - } - - @Test - fun `should serialize and deserialize ResourceTemplateReference correctly`() { - val resourceRef = ResourceTemplateReference(uri = "https://example.com/resource") - - val json = McpJson.encodeToString(resourceRef) - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals("ref/resource", decoded.type) - assertEquals("https://example.com/resource", decoded.uri) - } - - @Test - fun `should validate PromptReference`() { - val promptRef = PromptReference(name = "greeting") - - assertEquals("ref/prompt", promptRef.type) - assertEquals("greeting", promptRef.name) - } - - @Test - fun `should serialize and deserialize PromptReference correctly`() { - val promptRef = PromptReference(name = "test-prompt") - - val json = McpJson.encodeToString(promptRef) - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals("ref/prompt", decoded.type) - assertEquals("test-prompt", decoded.name) - } - - @Test - fun `should handle UnknownReference for invalid type`() { - val invalidJson = """{"type": "invalid_type"}""" - - val decoded = McpJson.decodeFromString(invalidJson) - - assertIs(decoded) - assertEquals("invalid_type", decoded.type) - } - - // PromptMessageContent Tests - @Test - fun `should validate text content`() { - val textContent = TextContent(text = "Hello, world!") - - assertEquals("text", textContent.type) - assertEquals("Hello, world!", textContent.text) - } - - @Test - fun `should serialize and deserialize text content correctly`() { - val textContent = TextContent(text = "Test message") - - val json = McpJson.encodeToString(textContent) - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals("text", decoded.type) - assertEquals("Test message", decoded.text) - } - - @Test - fun `should validate image content`() { - val imageContent = ImageContent( - data = "aGVsbG8=", // base64 encoded "hello" - mimeType = "image/png", - ) - - assertEquals("image", imageContent.type) - assertEquals("aGVsbG8=", imageContent.data) - assertEquals("image/png", imageContent.mimeType) - } - - @Test - fun `should serialize and deserialize image content correctly`() { - val imageContent = ImageContent( - data = "dGVzdA==", // base64 encoded "test" - mimeType = "image/jpeg", - ) - - val json = McpJson.encodeToString(imageContent) - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals("image", decoded.type) - assertEquals("dGVzdA==", decoded.data) - assertEquals("image/jpeg", decoded.mimeType) - } - - @Test - fun `should validate audio content`() { - val audioContent = AudioContent( - data = "aGVsbG8=", // base64 encoded "hello" - mimeType = "audio/mp3", - ) - - assertEquals("audio", audioContent.type) - assertEquals("aGVsbG8=", audioContent.data) - assertEquals("audio/mp3", audioContent.mimeType) - } - - @Test - fun `should serialize and deserialize audio content correctly`() { - val audioContent = AudioContent( - data = "YXVkaW8=", // base64 encoded "audio" - mimeType = "audio/wav", - ) - - val json = McpJson.encodeToString(audioContent) - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals("audio", decoded.type) - assertEquals("YXVkaW8=", decoded.data) - assertEquals("audio/wav", decoded.mimeType) - } - - @Test - fun `should validate embedded resource content`() { - val resource = TextResourceContents( - text = "File contents", - uri = "file:///path/to/file.txt", - mimeType = "text/plain", - ) - val embeddedResource = EmbeddedResource(resource = resource) - - assertEquals("resource", embeddedResource.type) - assertEquals(resource, embeddedResource.resource) - } - - @Test - fun `should serialize and deserialize embedded resource content correctly`() { - val resource = BlobResourceContents( - blob = "YmluYXJ5ZGF0YQ==", - uri = "file:///path/to/binary.dat", - mimeType = "application/octet-stream", - ) - val embeddedResource = EmbeddedResource(resource = resource) - - val json = McpJson.encodeToString(embeddedResource) - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals("resource", decoded.type) - assertIs(decoded.resource) - val decodedBlob = decoded.resource - assertEquals("YmluYXJ5ZGF0YQ==", decodedBlob.blob) - assertEquals("file:///path/to/binary.dat", decodedBlob.uri) - assertEquals("application/octet-stream", decodedBlob.mimeType) - } - - @Test - fun `should handle unknown content type`() { - val unknownJson = """{"type": "unknown_type"}""" - - val decoded = McpJson.decodeFromString(unknownJson) - - assertIs(decoded) - assertEquals("unknown_type", decoded.type) - } - - // PromptMessage Tests - @Test - fun `should validate prompt message with text content`() { - val textContent = TextContent(text = "Hello, assistant!") - val promptMessage = PromptMessage( - role = Role.user, - content = textContent, - ) - - assertEquals(Role.user, promptMessage.role) - assertEquals(textContent, promptMessage.content) - assertEquals("text", promptMessage.content.type) - } - - @Test - fun `should validate prompt message with embedded resource`() { - val resource = TextResourceContents( - text = "Primary application entry point", - uri = "file:///project/src/main.rs", - mimeType = "text/x-rust", - ) - val embeddedResource = EmbeddedResource(resource = resource) - val promptMessage = PromptMessage( - role = Role.assistant, - content = embeddedResource, - ) - - assertEquals(Role.assistant, promptMessage.role) - assertEquals("resource", promptMessage.content.type) - val content = promptMessage.content as EmbeddedResource - val textResource = content.resource as TextResourceContents - assertEquals("Primary application entry point", textResource.text) - assertEquals("file:///project/src/main.rs", textResource.uri) - assertEquals("text/x-rust", textResource.mimeType) - } - - @OptIn(ExperimentalTime::class) - @Test - fun `should serialize and deserialize annotations correctly`() { - val annotations = Annotations( - audience = listOf(Role.assistant), - lastModified = Instant.parse("2025-06-18T00:00:00Z"), - priority = 0.5, - ) - - val json = McpJson.encodeToString(annotations) - val decoded = McpJson.decodeFromString(json) - - assertEquals(listOf(Role.assistant), decoded.audience) - assertEquals(Instant.parse("2025-06-18T00:00:00Z"), decoded.lastModified) - assertEquals(0.5, decoded.priority) - } - - @Test - fun `should serialize and deserialize prompt message correctly`() { - val imageContent = ImageContent( - data = "aW1hZ2VkYXRh", // base64 encoded "imagedata" - mimeType = "image/png", - ) - val promptMessage = PromptMessage( - role = Role.assistant, - content = imageContent, - ) - - val json = McpJson.encodeToString(promptMessage) - val decoded = McpJson.decodeFromString(json) - - assertEquals(Role.assistant, decoded.role) - assertIs(decoded.content) - val decodedContent = decoded.content - assertEquals("aW1hZ2VkYXRh", decodedContent.data) - assertEquals("image/png", decodedContent.mimeType) - } - - // CallToolResult Tests - @Test - fun `should validate tool result with multiple content types`() { - val toolResult = CallToolResult( - content = listOf( - TextContent(text = "Found the following files:"), - EmbeddedResource( - resource = TextResourceContents( - text = "fn main() {}", - uri = "file:///project/src/main.rs", - mimeType = "text/x-rust", - ), - ), - EmbeddedResource( - resource = TextResourceContents( - text = "pub mod lib;", - uri = "file:///project/src/lib.rs", - mimeType = "text/x-rust", - ), - ), - ), - ) - - assertEquals(3, toolResult.content.size) - assertEquals("text", toolResult.content[0].type) - assertEquals("resource", toolResult.content[1].type) - assertEquals("resource", toolResult.content[2].type) - assertEquals(false, toolResult.isError) - } - - @Test - fun `should validate empty content array with default`() { - val toolResult = CallToolResult(content = emptyList()) - - assertEquals(0, toolResult.content.size) - assertEquals(false, toolResult.isError) - } - - @Test - fun `should serialize and deserialize CallToolResult correctly`() { - val toolResult = CallToolResult( - content = listOf( - TextContent(text = "Operation completed"), - ImageContent(data = "aW1hZ2U=", mimeType = "image/png"), - ), - isError = false, - ) - - val json = McpJson.encodeToString(toolResult) - val decoded = McpJson.decodeFromString(json) - - assertEquals(2, decoded.content.size) - assertIs(decoded.content[0]) - assertIs(decoded.content[1]) - assertEquals(false, decoded.isError) - } - - // CompleteRequest Tests - @Test - fun `should validate CompleteRequest with prompt reference`() { - val request = CompleteRequest( - ref = PromptReference(name = "greeting"), - argument = CompleteRequest.Argument(name = "name", value = "A"), - ) - - assertEquals("completion/complete", request.method.value) - assertIs(request.ref) - val promptRef = request.ref - assertEquals("greeting", promptRef.name) - assertEquals("name", request.argument.name) - assertEquals("A", request.argument.value) - } - - @Test - fun `should validate CompleteRequest with resource reference`() { - val request = CompleteRequest( - ref = ResourceTemplateReference(uri = "github://repos/{owner}/{repo}"), - argument = CompleteRequest.Argument(name = "repo", value = "t"), - ) - - assertEquals("completion/complete", request.method.value) - assertIs(request.ref) - val resourceRef = request.ref - assertEquals("github://repos/{owner}/{repo}", resourceRef.uri) - assertEquals("repo", request.argument.name) - assertEquals("t", request.argument.value) - } - - @Test - fun `should serialize and deserialize CompleteRequest correctly`() { - val request = CompleteRequest( - ref = PromptReference(name = "test"), - argument = CompleteRequest.Argument(name = "arg", value = ""), - ) - - val json = McpJson.encodeToString(request) - val decoded = McpJson.decodeFromString(json) - - assertEquals("completion/complete", decoded.method.value) - assertIs(decoded.ref) - val promptRef = decoded.ref - assertEquals("test", promptRef.name) - assertEquals("arg", decoded.argument.name) - assertEquals("", decoded.argument.value) - } - - @Test - fun `should validate CompleteRequest with complex URIs`() { - val request = CompleteRequest( - ref = ResourceTemplateReference(uri = "api://v1/{tenant}/{resource}/{id}"), - argument = CompleteRequest.Argument(name = "id", value = "123"), - ) - - val resourceRef = request.ref as ResourceTemplateReference - assertEquals("api://v1/{tenant}/{resource}/{id}", resourceRef.uri) - assertEquals("id", request.argument.name) - assertEquals("123", request.argument.value) - } -} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesUtilTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesUtilTest.kt deleted file mode 100644 index 5207af3..0000000 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesUtilTest.kt +++ /dev/null @@ -1,252 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk - -import io.modelcontextprotocol.kotlin.sdk.shared.McpJson -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.put -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertIs - -class TypesUtilTest { - - // ErrorCode Serializer Tests - @Test - fun `should serialize and deserialize ErrorCode Defined correctly`() { - val errorCode: ErrorCode = ErrorCode.Defined.InvalidRequest - - val json = McpJson.encodeToString(errorCode) - val decoded = McpJson.decodeFromString(json) - - assertEquals(ErrorCode.Defined.InvalidRequest, decoded) - assertEquals(-32600, decoded.code) - } - - @Test - fun `should serialize and deserialize ErrorCode Unknown correctly`() { - val errorCode: ErrorCode = ErrorCode.Unknown(1001) - - val json = McpJson.encodeToString(errorCode) - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals(1001, decoded.code) - } - - // Method Serializer Tests - @Test - fun `should serialize and deserialize Method Defined correctly`() { - val method: Method = Method.Defined.Initialize - - val json = McpJson.encodeToString(method) - val decoded = McpJson.decodeFromString(json) - - assertEquals(Method.Defined.Initialize, decoded) - assertEquals("initialize", decoded.value) - } - - @Test - fun `should serialize and deserialize Method Custom correctly`() { - val method: Method = Method.Custom("custom/method") - - val json = McpJson.encodeToString(method) - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals("custom/method", decoded.value) - } - - // StopReason Serializer Tests - @Test - fun `should serialize and deserialize StopReason EndTurn correctly`() { - val stopReason: StopReason = StopReason.EndTurn - - val json = McpJson.encodeToString(stopReason) - val decoded = McpJson.decodeFromString(json) - - assertEquals(StopReason.EndTurn, decoded) - assertEquals("endTurn", decoded.value) - } - - @Test - fun `should serialize and deserialize StopReason Other correctly`() { - val stopReason: StopReason = StopReason.Other("custom_reason") - - val json = McpJson.encodeToString(stopReason) - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals("custom_reason", decoded.value) - } - - // Reference Polymorphic Serializer Tests - @Test - fun `should deserialize ResourceTemplateReference polymorphically`() { - val json = """{"type": "ref/resource", "uri": "file:///test.txt"}""" - - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals("ref/resource", decoded.type) - assertEquals("file:///test.txt", decoded.uri) - } - - @Test - fun `should deserialize PromptReference polymorphically`() { - val json = """{"type": "ref/prompt", "name": "test-prompt"}""" - - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals("ref/prompt", decoded.type) - assertEquals("test-prompt", decoded.name) - } - - @Test - fun `should deserialize UnknownReference for invalid type`() { - val json = """{"type": "unknown_ref", "data": "test"}""" - - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals("unknown_ref", decoded.type) - } - - // PromptMessageContent Polymorphic Serializer Tests - @Test - fun `should deserialize TextContent polymorphically`() { - val json = """{"type": "text", "text": "Hello world"}""" - - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals("text", decoded.type) - assertEquals("Hello world", decoded.text) - } - - @Test - fun `should deserialize ImageContent polymorphically`() { - val json = """{"type": "image", "data": "aW1hZ2U=", "mimeType": "image/png"}""" - - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals("image", decoded.type) - assertEquals("aW1hZ2U=", decoded.data) - assertEquals("image/png", decoded.mimeType) - } - - @Test - fun `should deserialize AudioContent polymorphically`() { - val json = """{"type": "audio", "data": "YXVkaW8=", "mimeType": "audio/mp3"}""" - - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals("audio", decoded.type) - assertEquals("YXVkaW8=", decoded.data) - assertEquals("audio/mp3", decoded.mimeType) - } - - @Test - fun `should deserialize EmbeddedResource polymorphically`() { - val json = - """{"type": "resource", "resource": {"uri": "file:///test.txt", "mimeType": "text/plain", "text": "content"}}""" - - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals("resource", decoded.type) - assertIs(decoded.resource) - val textResource = decoded.resource - assertEquals("file:///test.txt", textResource.uri) - assertEquals("content", textResource.text) - } - - // ResourceContents Polymorphic Serializer Tests - @Test - fun `should deserialize TextResourceContents polymorphically`() { - val json = """{"uri": "file:///test.txt", "mimeType": "text/plain", "text": "file content"}""" - - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals("file:///test.txt", decoded.uri) - assertEquals("file content", decoded.text) - assertEquals("text/plain", decoded.mimeType) - } - - @Test - fun `should deserialize BlobResourceContents polymorphically`() { - val json = """{"uri": "file:///binary.dat", "mimeType": "application/octet-stream", "blob": "YmluYXJ5"}""" - - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals("file:///binary.dat", decoded.uri) - assertEquals("YmluYXJ5", decoded.blob) - assertEquals("application/octet-stream", decoded.mimeType) - } - - @Test - fun `should deserialize UnknownResourceContents for missing fields`() { - val json = """{"uri": "file:///unknown.dat", "mimeType": "unknown/type"}""" - - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals("file:///unknown.dat", decoded.uri) - assertEquals("unknown/type", decoded.mimeType) - } - - // RequestId Serializer Tests - @Test - fun `should serialize and deserialize RequestId StringId correctly`() { - val requestId: RequestId = RequestId.StringId("test-id") - - val json = McpJson.encodeToString(requestId) - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals("test-id", decoded.value) - } - - @Test - fun `should serialize and deserialize RequestId NumberId correctly`() { - val requestId: RequestId = RequestId.NumberId(42L) - - val json = McpJson.encodeToString(requestId) - val decoded = McpJson.decodeFromString(json) - - assertIs(decoded) - assertEquals(42L, decoded.value) - } - - // Utility Functions Tests - @Test - fun `should create CallToolResult ok correctly`() { - val result = CallToolResult.ok("Success message") - - assertEquals(listOf(TextContent("Success message")), result.content) - assertEquals(false, result.isError) - assertEquals(EmptyJsonObject, result._meta) - } - - @Test - fun `should create CallToolResult error correctly`() { - val result = CallToolResult.error("Error message") - - assertEquals(listOf(TextContent("Error message")), result.content) - assertEquals(true, result.isError) - assertEquals(EmptyJsonObject, result._meta) - } - - @Test - fun `should create CallToolResult with custom meta`() { - val meta = buildJsonObject { put("custom", "value") } - val result = CallToolResult.ok("Success", meta) - - assertEquals(listOf(TextContent("Success")), result.content) - assertEquals(false, result.isError) - assertEquals(meta, result._meta) - } -} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/models/ProgressNotificationsTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/models/ProgressNotificationsTest.kt new file mode 100644 index 0000000..9b52ef9 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/models/ProgressNotificationsTest.kt @@ -0,0 +1,72 @@ +package io.modelcontextprotocol.kotlin.sdk.models + +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.types.McpJson +import io.modelcontextprotocol.kotlin.sdk.types.ProgressNotification +import io.modelcontextprotocol.kotlin.sdk.types.ProgressNotificationParams +import io.modelcontextprotocol.kotlin.sdk.types.RequestId +import kotlin.test.Test + +class ProgressNotificationsTest { + + /** + * https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/progress#progress-flow + */ + @Test + fun `Read ProgressNotifications with string token`() { + //language=json + val json = """ + { + "jsonrpc": "2.0", + "method": "notifications/progress", + "params": { + "progressToken": "abc123", + "progress": 50, + "total": 100, + "message": "Reticulating splines..." + } + } + """.trimIndent() + + val result = McpJson.decodeFromString(json) + + result shouldBe ProgressNotification( + params = ProgressNotificationParams( + progressToken = RequestId.StringId("abc123"), + progress = 50.0, + message = "Reticulating splines...", + total = 100.0, + ), + ) + } + + /** + * https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/progress#progress-flow + */ + @Test + fun `Read ProgressNotifications with integer token`() { + //language=json + val json = """ + { + "jsonrpc": "2.0", + "method": "notifications/progress", + "params": { + "progressToken": 100500, + "progress": 50, + "total": 100, + "message": "Reticulating splines..." + } + } + """.trimIndent() + + val result = McpJson.decodeFromString(json) + result shouldBe ProgressNotification( + params = ProgressNotificationParams( + progressToken = RequestId.NumberId(100500), + progress = 50.0, + message = "Reticulating splines...", + total = 100.0, + ), + ) + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolConcurrencyTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolConcurrencyTest.kt new file mode 100644 index 0000000..b3c95ac --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolConcurrencyTest.kt @@ -0,0 +1,524 @@ +package io.modelcontextprotocol.kotlin.sdk.shared + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult +import io.modelcontextprotocol.kotlin.sdk.types.InitializedNotification +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.PingRequest +import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import io.modelcontextprotocol.kotlin.sdk.types.RequestId +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.async +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.coroutines.cancellation.CancellationException +import kotlin.test.Test +import kotlin.time.Duration.Companion.seconds + +class ProtocolConcurrencyTest { + + private fun TestScope.newProtocol(): TestProtocol = + TestProtocol(ProtocolOptions(handlerCoroutineContext = StandardTestDispatcher(testScheduler))) + + @Test + fun `a suspended slow handler does not block a later fast request`() = runTest { + val protocol = newProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + protocol.fallbackRequestHandler = { request, _ -> + if (request.method == "test/slow") delay(10.seconds) + EmptyResult() + } + + transport.deliver(JSONRPCRequest(id = RequestId(1L), method = "test/slow")) + transport.deliver(JSONRPCRequest(id = RequestId(2L), method = "test/fast")) + runCurrent() + + responsesOn(transport).map { it.id } shouldBe listOf(RequestId(2L)) + + advanceTimeBy(11.seconds) + runCurrent() + responsesOn(transport).map { it.id } shouldBe listOf(RequestId(2L), RequestId(1L)) + } + + @Test + fun `dispatch is serial until concurrent dispatch is enabled`() = runTest { + val protocol = newProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + // gate NOT flipped + + protocol.fallbackRequestHandler = { request, _ -> + if (request.method == "test/slow") delay(10.seconds) + EmptyResult() + } + + val delivery = launch { + transport.deliver(JSONRPCRequest(id = RequestId(1L), method = "test/slow")) + transport.deliver(JSONRPCRequest(id = RequestId(2L), method = "test/fast")) + } + runCurrent() + // inline handling: the delivering coroutine is parked inside the slow handler + delivery.isCompleted shouldBe false + responsesOn(transport) shouldBe emptyList() + + advanceTimeBy(11.seconds) + runCurrent() + delivery.isCompleted shouldBe true + responsesOn(transport).map { it.id } shouldBe listOf(RequestId(1L), RequestId(2L)) + } + + // the hook fires before handler lookup and cannot be disabled by a user handler + @Test + fun `initialized notification invokes the router hook even when a user handler is registered`() = runTest { + val protocol = newProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + + var userHandlerCalled = false + protocol.setNotificationHandler(Method.Defined.NotificationsInitialized) { + userHandlerCalled = true + COMPLETED + } + transport.deliver(JSONRPCNotification(method = Method.Defined.NotificationsInitialized.value)) + runCurrent() + + protocol.initializedNotificationCount shouldBe 1 + userHandlerCalled shouldBe true + } + + @Test + fun `cancelled notification cancels the in-flight handler and suppresses the response`() = runTest { + val protocol = newProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + val entered = CompletableDeferred() + val sawCancellation = CompletableDeferred() + protocol.fallbackRequestHandler = { _, _ -> + entered.complete(Unit) + try { + delay(10.seconds) + } catch (e: CancellationException) { + sawCancellation.complete(Unit) + throw e + } + EmptyResult() + } + + transport.deliver(JSONRPCRequest(id = RequestId(5L), method = "test/slow")) + runCurrent() + entered.await() + + transport.deliver(cancelledNotification(requestId = RequestId(5L), reason = "user gave up")) + runCurrent() + sawCancellation.await() + advanceUntilIdle() + + responsesOn(transport) shouldBe emptyList() + errorsOn(transport) shouldBe emptyList() // JSONRPCError messages on the wire + protocol.errors shouldBe emptyList() + } + + @Test + fun `cancelled notification for an unknown id is a silent no-op`() = runTest { + val protocol = newProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + transport.deliver(cancelledNotification(requestId = RequestId(404L), reason = "nope")) + advanceUntilIdle() + + protocol.errors shouldBe emptyList() + } + + @Test + fun `handler leaking CancellationException without being cancelled produces INTERNAL_ERROR`() = runTest { + val protocol = newProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + protocol.fallbackRequestHandler = { _, _ -> throw CancellationException("leaked") } + + transport.deliver(JSONRPCRequest(id = RequestId(6L), method = "test/leak")) + advanceUntilIdle() + + val error = errorsOn(transport).single() + error.id shouldBe RequestId(6L) + error.error.code shouldBe RPCError.ErrorCode.INTERNAL_ERROR + } + + // suppression race variants + @Test + fun `handler that swallows cancellation and returns normally still gets no response`() = runTest { + val protocol = newProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + val entered = CompletableDeferred() + protocol.fallbackRequestHandler = { _, _ -> + entered.complete(Unit) + try { + delay(10.seconds) + } catch (_: CancellationException) { + // swallow (anti-pattern) and return normally + } + EmptyResult() + } + + transport.deliver(JSONRPCRequest(id = RequestId(7L), method = "test/swallow")) + runCurrent() + entered.await() + transport.deliver(cancelledNotification(requestId = RequestId(7L), reason = "stop")) + advanceUntilIdle() + + responsesOn(transport) shouldBe emptyList() + } + + @Test + fun `handler throwing non-CE while cancelled gets no error response`() = runTest { + val protocol = newProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + val entered = CompletableDeferred() + protocol.fallbackRequestHandler = { _, _ -> + entered.complete(Unit) + try { + delay(10.seconds) + } catch (_: CancellationException) { + throw IllegalStateException("boom") + } + EmptyResult() + } + + transport.deliver(JSONRPCRequest(id = RequestId(8L), method = "test/throw")) + runCurrent() + entered.await() + transport.deliver(cancelledNotification(requestId = RequestId(8L), reason = "stop")) + advanceUntilIdle() + + errorsOn(transport) shouldBe emptyList() + protocol.errors shouldBe emptyList() + } + + @Test + fun `suspension-free notification handlers observe strict arrival order`() = runTest { + val protocol = newProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + val seen = mutableListOf() + protocol.fallbackNotificationHandler = { notification -> + seen.add((notification.params as JsonObject).getValue("i").jsonPrimitive.int) + } + + repeat(20) { i -> + transport.deliver( + JSONRPCNotification(method = "test/event", params = buildJsonObject { put("i", i) }), + ) + } + advanceUntilIdle() + + seen shouldBe (0 until 20).toList() + } + + @Test + fun `reconnect resets the dispatch gate to serial`() = runTest { + val protocol = newProtocol() + val transport1 = RecordingTransport() + protocol.connect(transport1) + protocol.enableConcurrency() + protocol.close() + + val transport2 = RecordingTransport() + protocol.connect(transport2) + protocol.fallbackRequestHandler = { request, _ -> + if (request.method == "test/slow") delay(10.seconds) + EmptyResult() + } + + val delivery = launch { + transport2.deliver(JSONRPCRequest(id = RequestId(1L), method = "test/slow")) + transport2.deliver(JSONRPCRequest(id = RequestId(2L), method = "test/fast")) + } + runCurrent() + delivery.isCompleted shouldBe false // serial again: gate reset + advanceTimeBy(11.seconds) + runCurrent() + responsesOn(transport2).map { it.id } shouldBe listOf(RequestId(1L), RequestId(2L)) + } + + @Test + fun `close cancels in-flight handlers suppresses responses and fails pending outbound requests`() = runTest { + val protocol = newProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + val entered = CompletableDeferred() + val sawCancellation = CompletableDeferred() + protocol.fallbackRequestHandler = { _, _ -> + entered.complete(Unit) + try { + delay(10.seconds) + } catch (e: CancellationException) { + sawCancellation.complete(Unit) + throw e + } + EmptyResult() + } + transport.deliver(JSONRPCRequest(id = RequestId(9L), method = "test/slow")) + runCurrent() + entered.await() + + // shouldThrow inside the async so its failure does not cancel the (non-supervisor) test scope. + val pending = async { shouldThrow { protocol.request(PingRequest()) } } + transport.awaitRequest() // outbound ping on the wire + + protocol.close() + runCurrent() + + sawCancellation.await() + responsesOn(transport) shouldBe emptyList() + val thrown = pending.await() + thrown.code shouldBe RPCError.ErrorCode.CONNECTION_CLOSED + } + + @Test + fun `message delivered after close is dropped without crash or response`() = runTest { + val protocol = newProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + protocol.close() + + transport.deliver(JSONRPCRequest(id = RequestId(1L), method = "test/anything")) + advanceUntilIdle() + + responsesOn(transport) shouldBe emptyList() + errorsOn(transport) shouldBe emptyList() + } + + @Test + fun `at most maxConcurrentHandlers handlers run concurrently while control messages bypass`() = runTest { + val protocol = TestProtocol( + ProtocolOptions( + handlerCoroutineContext = StandardTestDispatcher(testScheduler), + maxConcurrentHandlers = 2, + ), + ) + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + var entered = 0 + val release = CompletableDeferred() + protocol.fallbackRequestHandler = { _, _ -> + entered += 1 + release.await() + EmptyResult() + } + + repeat(4) { i -> transport.deliver(JSONRPCRequest(id = RequestId(i.toLong()), method = "test/slow")) } + runCurrent() + entered shouldBe 2 // exactly two running, two parked on the semaphore + + // ping (bypass) is answered while saturated + transport.deliver(JSONRPCRequest(id = RequestId(100L), method = Method.Defined.Ping.value)) + runCurrent() + responsesOn(transport).map { it.id } shouldBe listOf(RequestId(100L)) + + // responses (bypass, inline) are processed while saturated + val pending = async { protocol.request(PingRequest()) } + runCurrent() + val outbound = transport.awaitRequest() + transport.deliver(JSONRPCResponse(id = outbound.id, result = EmptyResult())) + runCurrent() + pending.isCompleted shouldBe true + + release.complete(Unit) + advanceUntilIdle() + entered shouldBe 4 + responsesOn(transport) shouldHaveSize 5 + } + + @Test + fun `flood beyond maxInFlightHandlers is rejected fail-fast and the read loop never suspends`() = runTest { + val protocol = TestProtocol( + ProtocolOptions( + handlerCoroutineContext = StandardTestDispatcher(testScheduler), + maxConcurrentHandlers = 1, + maxInFlightHandlers = 4, + ), + ) + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + val release = CompletableDeferred() + protocol.fallbackRequestHandler = { _, _ -> + release.await() + EmptyResult() + } + + // 10 requests delivered back-to-back; delivery must complete without advancing + // virtual time — i.e. the read loop was never suspended on admission. + val delivery = launch { + repeat(10) { i -> transport.deliver(JSONRPCRequest(id = RequestId(i.toLong()), method = "test/slow")) } + } + runCurrent() + delivery.isCompleted shouldBe true + + // 4 admitted (1 running + 3 parked), 6 rejected immediately + val busyErrors = errorsOn(transport) + busyErrors shouldHaveSize 6 + busyErrors.forEach { + it.error.code shouldBe RPCError.ErrorCode.INTERNAL_ERROR + it.error.message shouldBe "Server is busy: too many in-flight messages" + } + + // ping still answered at full saturation + transport.deliver(JSONRPCRequest(id = RequestId(100L), method = Method.Defined.Ping.value)) + runCurrent() + responsesOn(transport).map { it.id } shouldBe listOf(RequestId(100L)) + + release.complete(Unit) + advanceUntilIdle() + responsesOn(transport) shouldHaveSize 5 // ping + the 4 admitted + } + + @Test + fun `overflowing notifications are dropped and reported via onError`() = runTest { + val protocol = TestProtocol( + ProtocolOptions( + handlerCoroutineContext = StandardTestDispatcher(testScheduler), + maxConcurrentHandlers = 1, + maxInFlightHandlers = 2, + ), + ) + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + // parks one handler with a request: holds the sole execution permit and one in-flight slot + val release = CompletableDeferred() + protocol.fallbackRequestHandler = { _, _ -> + release.await() + EmptyResult() + } + transport.deliver(JSONRPCRequest(id = RequestId(1L), method = "test/hog")) + runCurrent() + + // notification 1 is admitted (parked on the semaphore, behind the hogged permit); + // notification 2 overflows the admission cap and is dropped instead of parked. + val notificationRelease = CompletableDeferred() + protocol.fallbackNotificationHandler = { notificationRelease.await() } + transport.deliver(JSONRPCNotification(method = "test/event")) + transport.deliver(JSONRPCNotification(method = "test/event")) + runCurrent() + + protocol.errors shouldHaveSize 1 + protocol.errors.single().message.orEmpty() shouldContain "too many in-flight messages" + errorsOn(transport) shouldBe emptyList() // no wire error for dropped notifications + + release.complete(Unit) + notificationRelease.complete(Unit) + advanceUntilIdle() + } + + @Test + fun `cancelling saturating handlers releases permits and lets parked handlers run`() = runTest { + val protocol = TestProtocol( + ProtocolOptions( + handlerCoroutineContext = StandardTestDispatcher(testScheduler), + maxConcurrentHandlers = 1, + ), + ) + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + val secondCompleted = CompletableDeferred() + protocol.fallbackRequestHandler = { request, _ -> + if (request.method == "test/hog") { + delay(1000.seconds) // cancellable park + error("unreachable") + } + secondCompleted.complete(Unit) + EmptyResult() + } + + transport.deliver(JSONRPCRequest(id = RequestId(1L), method = "test/hog")) // holds the permit + transport.deliver(JSONRPCRequest(id = RequestId(2L), method = "test/next")) // parked on the semaphore + runCurrent() + secondCompleted.isCompleted shouldBe false + + // cancel the hog THROUGH the bypass while saturated + transport.deliver(cancelledNotification(requestId = RequestId(1L), reason = "make room")) + advanceUntilIdle() + + secondCompleted.isCompleted shouldBe true + responsesOn(transport).map { it.id } shouldBe listOf(RequestId(2L)) // hog suppressed, next answered + } + + @Test + fun `a parked handler can itself be cancelled before it ever runs`() = runTest { + val protocol = TestProtocol( + ProtocolOptions( + handlerCoroutineContext = StandardTestDispatcher(testScheduler), + maxConcurrentHandlers = 1, + ), + ) + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + protocol.fallbackRequestHandler = { request, _ -> + if (request.method == "test/hog") { + delay(1000.seconds) // cancellable park + error("unreachable") + } + EmptyResult() + } + + transport.deliver(JSONRPCRequest(id = RequestId(1L), method = "test/hog")) // holds the permit + transport.deliver(JSONRPCRequest(id = RequestId(2L), method = "test/next")) // parked on the semaphore + runCurrent() + + // cancel the parked handler before it ever acquires the permit, then cancel the hog too + transport.deliver(cancelledNotification(requestId = RequestId(2L), reason = "give up while parked")) + transport.deliver(cancelledNotification(requestId = RequestId(1L), reason = "make room")) + advanceUntilIdle() + + // no response for id=2 at all: its withPermit acquire was cancelled before onRequest ever ran + responsesOn(transport) shouldBe emptyList() + errorsOn(transport) shouldBe emptyList() + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptionsTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptionsTest.kt new file mode 100644 index 0000000..81bf24a --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptionsTest.kt @@ -0,0 +1,16 @@ +package io.modelcontextprotocol.kotlin.sdk.shared + +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.Dispatchers +import kotlin.test.Test + +class ProtocolOptionsTest { + + @Test + fun `defaults expose concurrency knobs`() { + val options = ProtocolOptions() + options.handlerCoroutineContext shouldBe Dispatchers.Default + options.maxConcurrentHandlers shouldBe 64 + options.maxInFlightHandlers shouldBe 256 + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOutboundCancellationTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOutboundCancellationTest.kt new file mode 100644 index 0000000..38ad556 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOutboundCancellationTest.kt @@ -0,0 +1,167 @@ +package io.modelcontextprotocol.kotlin.sdk.shared + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import io.modelcontextprotocol.kotlin.sdk.types.CancelledNotificationParams +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse +import io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.McpJson +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.PingRequest +import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import io.modelcontextprotocol.kotlin.sdk.types.RequestId +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.async +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withTimeout +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long +import kotlin.coroutines.cancellation.CancellationException +import kotlin.test.Test +import kotlin.time.Duration.Companion.seconds + +class ProtocolOutboundCancellationTest { + + @Test + fun `request times out awaiting the response and sends CancelledNotification with timeout reason`() = runTest { + val protocol = TestProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + + val inFlight = async { + shouldThrow { + protocol.request(PingRequest(), RequestOptions(timeout = 5.seconds)) + } + } + val sent = transport.awaitRequest() // request is on the wire; peer never responds + advanceTimeBy(6.seconds) + runCurrent() + + val thrown = inFlight.await() + thrown.code shouldBe RPCError.ErrorCode.REQUEST_TIMEOUT + thrown.data?.jsonObject?.get("timeout")?.jsonPrimitive?.long shouldBe 5.seconds.inWholeMilliseconds + + val cancelledJson = transport.sentWithOptions + .map { it.first } + .filterIsInstance() + .single { it.method == Method.Defined.NotificationsCancelled.value } + val cancelledParams = McpJson.decodeFromJsonElement(cancelledJson.params!!) + cancelledParams.requestId shouldBe sent.id + cancelledParams.reason.shouldNotBeNull() shouldContain "timed out" + } + + @Test + fun `outer withTimeout around request propagates the original timeout exception and notifies the peer`() = runTest { + val protocol = TestProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + + val inFlight = async { + shouldThrow { + withTimeout(1.seconds) { + protocol.request(PingRequest()) + } + } + } + transport.awaitRequest() // request is on the wire; peer never responds + advanceTimeBy(2.seconds) + runCurrent() + inFlight.await() + + transport.sentWithOptions.map { it.first }.filterIsInstance() + .filter { it.method == Method.Defined.NotificationsCancelled.value } shouldHaveSize 1 + } + + @Test + fun `caller cancellation sends CancelledNotification and rethrows the original exception`() = runTest { + val protocol = TestProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + + val cancelled = CompletableDeferred() + val job = launch { + try { + protocol.request(PingRequest()) + } catch (e: CancellationException) { + cancelled.complete(e) + throw e + } + } + transport.awaitRequest() + job.cancelAndJoin() + + cancelled.await() + val wire = transport.sentWithOptions.map { it.first }.filterIsInstance() + .filter { it.method == Method.Defined.NotificationsCancelled.value } + wire shouldHaveSize 1 + } + + @Test + fun `cancelling the initialize request performs local cleanup but sends no CancelledNotification`() = runTest { + val protocol = TestProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + + val job = launch { + protocol.request( + InitializeRequest( + InitializeRequestParams( + protocolVersion = LATEST_PROTOCOL_VERSION, + capabilities = ClientCapabilities(), + clientInfo = Implementation(name = "t", version = "1"), + ), + ), + ) + } + val sent = transport.awaitRequest() + sent.method shouldBe Method.Defined.Initialize.value + job.cancelAndJoin() + + transport.sentWithOptions.map { it.first }.filterIsInstance() + .filter { it.method == Method.Defined.NotificationsCancelled.value } shouldHaveSize 0 + protocol.responseHandlers shouldBe emptyMap() + } + + @Test + fun `late response for a cancelled request is ignored silently`() = runTest { + val protocol = TestProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + + val job = launch { protocol.request(PingRequest()) } + val sent = transport.awaitRequest() + job.cancelAndJoin() + + transport.deliver(JSONRPCResponse(id = sent.id, result = EmptyResult())) + + protocol.errors shouldHaveSize 0 // no "unknown message ID" onError + } + + @Test + fun `response for a genuinely unknown id still reports onError`() = runTest { + val protocol = TestProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + + transport.deliver(JSONRPCResponse(id = RequestId(999L), result = EmptyResult())) + + protocol.errors shouldHaveSize 1 + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolTest.kt new file mode 100644 index 0000000..2846ccc --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolTest.kt @@ -0,0 +1,370 @@ +package io.modelcontextprotocol.kotlin.sdk.shared + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.types.shouldBeInstanceOf +import io.modelcontextprotocol.kotlin.sdk.types.CustomRequest +import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse +import io.modelcontextprotocol.kotlin.sdk.types.McpJson +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.PingRequest +import io.modelcontextprotocol.kotlin.sdk.types.ProgressNotification +import io.modelcontextprotocol.kotlin.sdk.types.ProgressNotificationParams +import io.modelcontextprotocol.kotlin.sdk.types.ProgressToken +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.RequestId +import io.modelcontextprotocol.kotlin.sdk.types.RequestMeta +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonObjectBuilder +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.encodeToJsonElement +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlin.coroutines.cancellation.CancellationException +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.time.Duration.Companion.seconds + +class ProtocolTest { + private lateinit var protocol: TestProtocol + private lateinit var transport: RecordingTransport + + @BeforeTest + fun setUp() { + protocol = TestProtocol() + transport = RecordingTransport() + } + + @Test + fun `should preserve existing meta when adding progress token`() = runTest { + protocol.connect(transport) + val request = ReadResourceRequest( + ReadResourceRequestParams( + uri = "test://resource", + meta = metaOf { + put("customField", JsonPrimitive("customValue")) + put("anotherField", JsonPrimitive(123)) + }, + ), + ) + + val inFlight = async { + protocol.request( + request = request, + options = RequestOptions(onProgress = {}), + ) + } + + val sent = transport.awaitRequest() + val params = sent.params?.jsonObject.shouldNotBeNull() + val meta = params["_meta"]?.jsonObject.shouldNotBeNull() + + params["uri"]?.jsonPrimitive?.content shouldBe "test://resource" + meta["customField"]?.jsonPrimitive?.content shouldBe "customValue" + meta["anotherField"]?.jsonPrimitive?.int shouldBe 123 + meta["progressToken"] shouldBe McpJson.encodeToJsonElement(sent.id) + + transport.deliver(JSONRPCResponse(sent.id, EmptyResult())) + inFlight.await() + } + + @Test + fun `should create meta with progress token when none exists`() = runTest { + protocol.connect(transport) + val request = ReadResourceRequest( + ReadResourceRequestParams( + uri = "test://resource", + meta = null, + ), + ) + + val inFlight = async { + protocol.request( + request = request, + options = RequestOptions(onProgress = {}), + ) + } + + val sent = transport.awaitRequest() + val params = sent.params?.jsonObject.shouldNotBeNull() + val meta = params["_meta"]?.jsonObject.shouldNotBeNull() + + params["uri"]?.jsonPrimitive?.content shouldBe "test://resource" + meta["progressToken"] shouldBe McpJson.encodeToJsonElement(sent.id) + + transport.deliver(JSONRPCResponse(sent.id, EmptyResult())) + inFlight.await() + } + + @Test + fun `should not modify meta when onProgress is absent`() = runTest { + protocol.connect(transport) + val originalMeta = metaJson { + put("customField", JsonPrimitive("customValue")) + } + val request = ReadResourceRequest( + ReadResourceRequestParams( + uri = "test://resource", + meta = RequestMeta(originalMeta), + ), + ) + + val inFlight = async { + protocol.request(request) + } + + val sent = transport.awaitRequest() + val params = sent.params?.jsonObject.shouldNotBeNull() + val meta = params["_meta"]?.jsonObject.shouldNotBeNull() + + meta shouldBe originalMeta + params["uri"]?.jsonPrimitive?.content shouldBe "test://resource" + + transport.deliver(JSONRPCResponse(sent.id, EmptyResult())) + inFlight.await() + } + + @Test + fun `should propagate CancellationException from notification handler without calling onError`() = runTest { + protocol.connect(transport) + + protocol.fallbackNotificationHandler = { + throw CancellationException("test cancellation") + } + + shouldThrow { + transport.deliver(JSONRPCNotification(method = "test/notification")) + } + + protocol.errors shouldHaveSize 0 + } + + @Test + fun `should report non-cancellation exception from notification handler via onError`() = runTest { + protocol.connect(transport) + + protocol.fallbackNotificationHandler = { + throw IllegalStateException("handler failed") + } + + // Non-CE exceptions are caught and reported, not propagated + transport.deliver(JSONRPCNotification(method = "test/notification")) + + protocol.errors shouldHaveSize 1 + protocol.errors[0].message shouldBe "handler failed" + } + + @Test + fun `should create params object when request params are null`() = runTest { + protocol.connect(transport) + val request = CustomRequest( + method = Method.Custom("example"), + params = null, + ) + + val inFlight = async { + protocol.request( + request = request, + options = RequestOptions(onProgress = {}), + ) + } + + val sent = transport.awaitRequest() + val params = sent.params?.jsonObject.shouldNotBeNull() + val meta = params["_meta"]?.jsonObject.shouldNotBeNull() + + params.keys shouldContainExactly setOf("_meta") + meta["progressToken"] shouldBe McpJson.encodeToJsonElement(sent.id) + + transport.deliver(JSONRPCResponse(sent.id, EmptyResult())) + inFlight.await() + } + + @Test + fun `request handler receives enriched extra and ambient context element`() = runTest { + protocol.connect(transport) + + var seenByParameter: RequestHandlerExtra? = null + var seenAmbient: RequestHandlerExtra? = null + protocol.fallbackRequestHandler = { _, extra -> + seenByParameter = extra + seenAmbient = currentRequestHandlerExtra() + EmptyResult() + } + + transport.deliver(JSONRPCRequest(id = RequestId(7L), method = "custom/echo")) + + seenByParameter shouldNotBe null + seenByParameter?.requestId shouldBe RequestId(7L) + seenByParameter?.method shouldBe Method.Custom("custom/echo") + // the SAME instance flows through both delivery paths + seenAmbient shouldBe seenByParameter + } + + @Test + fun `extra method resolves defined methods to Defined entries`() = runTest { + protocol.connect(transport) + + var seenMethod: Method? = null + protocol.setRequestHandler(Method.Defined.Ping) { _, extra -> + seenMethod = extra.method + EmptyResult() + } + transport.deliver(JSONRPCRequest(id = RequestId(1L), method = "ping")) + + seenMethod shouldBe Method.Defined.Ping + } + + @Test + fun `extra sendNotification stamps relatedRequestId`() = runTest { + protocol.connect(transport) + + protocol.fallbackRequestHandler = { _, extra -> + extra.sendNotification( + ProgressNotification( + ProgressNotificationParams(progressToken = ProgressToken(1L), progress = 0.5), + ), + ) + EmptyResult() + } + + transport.deliver(JSONRPCRequest(id = RequestId(42L), method = "custom/progressing")) + + val sent = transport.sentWithOptions.first { it.first is JSONRPCNotification } + sent.second?.relatedRequestId shouldBe RequestId(42L) + } + + @Test + fun `extra sendRequest stamps relatedRequestId and preserves caller options`() = runTest { + protocol.connect(transport) + + protocol.fallbackRequestHandler = { _, extra -> + extra.sendRequest( + PingRequest(), + RequestOptions(resumptionToken = "tok", onProgress = { }, timeout = 30.seconds), + ) + EmptyResult() + } + + // The serial phase runs the handler inline inside deliver(), and the handler suspends + // awaiting the nested request's response — drive delivery from a child coroutine. + val delivery = launch { + transport.deliver(JSONRPCRequest(id = RequestId(42L), method = "custom/nested")) + } + + val outbound = transport.awaitRequest() + val recorded = transport.sentWithOptions + .first { (it.first as? JSONRPCRequest)?.id == outbound.id } + .second + val options = recorded.shouldBeInstanceOf() + options.relatedRequestId shouldBe RequestId(42L) + options.resumptionToken shouldBe "tok" + options.timeout shouldBe 30.seconds + options.onProgress shouldNotBe null + + transport.deliver(JSONRPCResponse(id = outbound.id, result = EmptyResult())) + delivery.join() + } + + @Test + fun `currentRequestHandlerExtra returns null outside handlers`() = runTest { + currentRequestHandlerExtra() shouldBe null + } + + @Test + fun `connect while already connected throws IllegalStateException`() = runTest { + protocol.connect(transport) + shouldThrow { + protocol.connect(RecordingTransport()) + } + } + + @Test + fun `stale onClose from a previous transport does not tear down the successor connection`() = runTest { + protocol.connect(transport) + val staleCloseCallback = transport.closeCallback ?: error("onClose not registered") + + protocol.close() // disconnect first transport (fires its own doClose) + val second = RecordingTransport() + protocol.connect(second) // reconnect + + staleCloseCallback() // late duplicate close signal from transport #1 + + // successor connection must still be alive + protocol.transport shouldBe second + } + + @Test + fun `failed transport start rolls back the connection and allows reconnect`() = runTest { + val failing = object : Transport { + override suspend fun start(): Unit = error("boom") + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) {} + override suspend fun close() {} + override fun onClose(block: () -> Unit) {} + override fun onError(block: (Throwable) -> Unit) {} + override fun onMessage(block: suspend (JSONRPCMessage) -> Unit) {} + } + shouldThrow { protocol.connect(failing) } + protocol.transport shouldBe null // rolled back + protocol.connect(transport) // reconnect succeeds, no "already connected" + protocol.transport shouldBe transport + } + + @Test + fun `request cleans up its handlers when the transport send fails`() = runTest { + // Send fails without closing the connection, so the leak is not masked by doClose(). + val failingSend = object : Transport { + override suspend fun start() {} + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?): Unit = + throw IllegalStateException("send failed") + override suspend fun close() {} + override fun onClose(block: () -> Unit) {} + override fun onError(block: (Throwable) -> Unit) {} + override fun onMessage(block: suspend (JSONRPCMessage) -> Unit) {} + } + protocol.connect(failingSend) + + shouldThrow { + protocol.request(PingRequest(), RequestOptions(onProgress = {})) + } + + protocol.responseHandlers.size shouldBe 0 + protocol.progressHandlers.size shouldBe 0 + } + + @Test + fun `connect rejects non-positive maxConcurrentHandlers`() = runTest { + val error = shouldThrow { + TestProtocol(ProtocolOptions(maxConcurrentHandlers = 0)).connect(RecordingTransport()) + } + error.message.shouldNotBeNull() shouldContain "maxConcurrentHandlers" + } + + @Test + fun `connect rejects maxInFlightHandlers below maxConcurrentHandlers`() = runTest { + val error = shouldThrow { + TestProtocol(ProtocolOptions(maxConcurrentHandlers = 4, maxInFlightHandlers = 0)) + .connect(RecordingTransport()) + } + error.message.shouldNotBeNull() shouldContain "maxInFlightHandlers" + } +} + +private fun metaOf(builderAction: JsonObjectBuilder.() -> Unit): RequestMeta = RequestMeta(metaJson(builderAction)) + +private fun metaJson(builderAction: JsonObjectBuilder.() -> Unit): JsonObject = buildJsonObject(builderAction) diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolTestFakes.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolTestFakes.kt new file mode 100644 index 0000000..9e8dd60 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolTestFakes.kt @@ -0,0 +1,112 @@ +package io.modelcontextprotocol.kotlin.sdk.shared + +import io.modelcontextprotocol.kotlin.sdk.types.CancelledNotificationParams +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCError +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse +import io.modelcontextprotocol.kotlin.sdk.types.McpJson +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.RequestId +import kotlinx.coroutines.channels.Channel +import kotlinx.serialization.json.encodeToJsonElement + +/** + * Shared [Protocol] test double: records [onError] calls, counts `notifications/initialized` + * router-hook invocations, and no-ops all capability assertions. + * + * The concurrency gate is flipped only via [enableConcurrency]; nothing in this double flips it + * implicitly, so tests keep today's serial semantics unless they opt in. + */ +internal class TestProtocol(options: ProtocolOptions? = null) : Protocol(options) { + val errors = mutableListOf() + var initializedNotificationCount = 0 + + fun enableConcurrency() { + enableConcurrentDispatch() + } + + override fun onInitializedNotification() { + initializedNotificationCount++ + // deliberately does NOT flip the gate — tests control the flip explicitly + } + + override fun onError(error: Throwable) { + errors.add(error) + } + + override fun assertCapabilityForMethod(method: Method) { + // noop + } + override fun assertNotificationCapability(method: Method) { + // noop + } + public override fun assertRequestHandlerCapability(method: Method) { + // noop + } +} + +/** Shared [Transport] test double: records every sent message (with its options) and replays inbound ones. */ +internal class RecordingTransport : Transport { + val sentMessages = Channel(Channel.UNLIMITED) + private var onMessageCallback: (suspend (JSONRPCMessage) -> Unit)? = null + private var onCloseCallback: (() -> Unit)? = null + + val sentWithOptions = mutableListOf>() + + var closeCallback: (() -> Unit)? = null + private set + + override suspend fun start() { + // noop + } + + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { + sentWithOptions.add(message to options) + sentMessages.send(message) + } + + override suspend fun close() { + onCloseCallback?.invoke() + } + + override fun onClose(block: () -> Unit) { + closeCallback = block + onCloseCallback = block + } + + override fun onError(block: (Throwable) -> Unit) { + // noop + } + + override fun onMessage(block: suspend (JSONRPCMessage) -> Unit) { + onMessageCallback = block + } + + suspend fun awaitRequest(): JSONRPCRequest { + while (true) { + val message = sentMessages.receive() + if (message is JSONRPCRequest) return message + } + } + + /** Cross-thread-safe reader used by real-dispatcher tests; [sentWithOptions] is single-thread only. */ + suspend fun awaitSent(): JSONRPCMessage = sentMessages.receive() + + suspend fun deliver(message: JSONRPCMessage) { + val callback = onMessageCallback ?: error("onMessage callback not registered") + callback(message) + } +} + +internal fun responsesOn(transport: RecordingTransport): List = + transport.sentWithOptions.map { it.first }.filterIsInstance() + +internal fun errorsOn(transport: RecordingTransport): List = + transport.sentWithOptions.map { it.first }.filterIsInstance() + +internal fun cancelledNotification(requestId: RequestId, reason: String): JSONRPCNotification = JSONRPCNotification( + method = Method.Defined.NotificationsCancelled.value, + params = McpJson.encodeToJsonElement(CancelledNotificationParams(requestId = requestId, reason = reason)), +) diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferTest.kt index 8e6f4f6..e5e50f8 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferTest.kt @@ -2,11 +2,13 @@ package io.modelcontextprotocol.kotlin.sdk.shared import io.ktor.utils.io.charsets.Charsets import io.ktor.utils.io.core.toByteArray -import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage -import io.modelcontextprotocol.kotlin.sdk.JSONRPCNotification +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification import kotlinx.serialization.json.Json import kotlin.test.Test +import kotlin.test.assertContains import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertNull class ReadBufferTest { @@ -31,6 +33,8 @@ class ReadBufferTest { val messageBytes = json.encodeToString(testMessage).encodeToByteArray() readBuffer.append(messageBytes) assertNull(readBuffer.readMessage()) + readBuffer.append("\r".encodeToByteArray()) + assertNull(readBuffer.readMessage()) // Append a newline and verify message is now available readBuffer.append("\n".encodeToByteArray()) @@ -45,6 +49,20 @@ class ReadBufferTest { assertNull(readBuffer.readMessage()) } + @Test + fun `skip blank line`() { + val readBuffer = ReadBuffer() + readBuffer.append(" \n".toByteArray()) + assertNull(readBuffer.readMessage()) + } + + @Test + fun `skip invalid json line`() { + val readBuffer = ReadBuffer() + readBuffer.append(" {ah=oh\n".toByteArray()) + assertNull(readBuffer.readMessage()) + } + @Test fun `should be reusable after clearing`() { val readBuffer = ReadBuffer() @@ -59,4 +77,35 @@ class ReadBufferTest { val message = readBuffer.readMessage() assertEquals(testMessage, message) } + + @Test + fun `should fail when an unframed blob exceeds the cap`() { + val readBuffer = ReadBuffer(maxFrameSize = 64) + // No newline ever arrives: the memory-exhaustion vector. + readBuffer.append(ByteArray(100) { 'a'.code.toByte() }) + val ex = assertFailsWith { readBuffer.readMessage() } + assertContains(ex.message.orEmpty(), "maximum size") + } + + @Test + fun `should fail when a completed line exceeds the cap`() { + val readBuffer = ReadBuffer(maxFrameSize = 64) + readBuffer.append(ByteArray(100) { 'a'.code.toByte() } + '\n'.code.toByte()) + assertFailsWith { readBuffer.readMessage() } + } + + @Test + fun `should not enforce a cap when maxFrameSize is non-positive`() { + val readBuffer = ReadBuffer(maxFrameSize = 0) + // Well beyond any small cap and still no newline — must not throw when disabled. + readBuffer.append(ByteArray(8192) { 'a'.code.toByte() }) + assertNull(readBuffer.readMessage()) + } + + @Test + fun `should parse a message that fits under the cap`() { + val readBuffer = ReadBuffer(maxFrameSize = 1024) + readBuffer.append(serializeMessage(testMessage).encodeToByteArray()) + assertEquals(testMessage, readBuffer.readMessage()) + } } diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CapabilitiesTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CapabilitiesTest.kt new file mode 100644 index 0000000..3468aae --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CapabilitiesTest.kt @@ -0,0 +1,946 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.kotest.assertions.json.shouldEqualJson +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerializationRoundTrip +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class CapabilitiesTest { + + @Test + fun `should serialize Implementation with minimal fields`() { + val implementation = Implementation( + name = "test-server", + version = "1.0.0", + ) + verifySerialization( + implementation, + McpJson, + """ + { + "name": "test-server", + "version": "1.0.0" + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize Implementation with all fields`() { + val implementation = Implementation( + name = "test-server", + version = "1.0.0", + title = "Test Server", + websiteUrl = "https://example.com", + icons = listOf( + Icon(src = "https://example.com/icon.png"), + ), + ) + verifySerialization( + implementation, + McpJson, + """ + { + "name": "test-server", + "version": "1.0.0", + "title": "Test Server", + "websiteUrl": "https://example.com", + "icons": [ + {"src": "https://example.com/icon.png"} + ] + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize Implementation from JSON`() { + val json = """ + { + "name": "test-server", + "version": "2.1.3-beta", + "title": "Test Server", + "websiteUrl": "https://example.com" + } + """.trimIndent() + + val implementation = verifyDeserialization(McpJson, json) + + assertEquals("test-server", implementation.name) + assertEquals("2.1.3-beta", implementation.version) + assertEquals("Test Server", implementation.title) + assertEquals("https://example.com", implementation.websiteUrl) + } + + @Test + fun `should deserialize Implementation with minimal fields from JSON`() { + val json = """ + { + "name": "minimal-server", + "version": "0.1.0" + } + """.trimIndent() + + val implementation = verifyDeserialization(McpJson, json) + + assertEquals("minimal-server", implementation.name) + assertEquals("0.1.0", implementation.version) + assertNull(implementation.title) + assertNull(implementation.websiteUrl) + assertNull(implementation.icons) + } + + @Test + fun `should serialize and deserialize Implementation round trip`() { + val original = Implementation( + name = "round-trip-test", + version = "1.2.3", + title = "Round Trip Test", + websiteUrl = "https://test.com", + ) + + verifySerializationRoundTrip(original, McpJson) + } + + // ClientCapabilities tests + @Test + fun `should serialize empty ClientCapabilities`() { + val capabilities = ClientCapabilities() + val json = McpJson.encodeToString(capabilities) + + json shouldEqualJson "{}" + } + + @Test + fun `should serialize ClientCapabilities with sampling`() { + val capabilities = ClientCapabilities( + sampling = ClientCapabilities.Sampling(), + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "sampling": {} + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ClientCapabilities with roots without listChanged`() { + val capabilities = ClientCapabilities( + roots = ClientCapabilities.Roots(), + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "roots": {} + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ClientCapabilities with roots with listChanged`() { + val capabilities = ClientCapabilities( + roots = ClientCapabilities.Roots(listChanged = true), + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "roots": { + "listChanged": true + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ClientCapabilities with elicitation`() { + val capabilities = ClientCapabilities( + elicitation = ClientCapabilities.elicitation, + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "elicitation": {} + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ClientCapabilities with extensions`() { + val extensions = mapOf( + "io.modelcontextprotocol/ui" to buildJsonObject { + put("mimeTypes", buildJsonObject { put("text/html", true) }) + }, + ) + val capabilities = ClientCapabilities( + extensions = extensions, + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "extensions": { + "io.modelcontextprotocol/ui": { + "mimeTypes": { + "text/html": true + } + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ClientCapabilities with empty extensions settings`() { + val capabilities = ClientCapabilities( + extensions = mapOf( + "io.modelcontextprotocol/ui" to EmptyJsonObject, + ), + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "extensions": { + "io.modelcontextprotocol/ui": {} + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ClientCapabilities with experimental`() { + val experimental = buildJsonObject { + put( + "customFeature", + buildJsonObject { + put("enabled", true) + }, + ) + } + val capabilities = ClientCapabilities( + experimental = experimental, + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "experimental": { + "customFeature": { + "enabled": true + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ClientCapabilities with all fields`() { + val experimental = buildJsonObject { + put("feature1", buildJsonObject { put("enabled", true) }) + } + val extensions = mapOf( + "io.modelcontextprotocol/ui" to EmptyJsonObject, + ) + val capabilities = ClientCapabilities( + sampling = ClientCapabilities.Sampling(), + roots = ClientCapabilities.Roots(listChanged = true), + elicitation = ClientCapabilities.elicitation, + experimental = experimental, + extensions = extensions, + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "sampling": {}, + "roots": { + "listChanged": true + }, + "elicitation": {}, + "experimental": { + "feature1": { + "enabled": true + } + }, + "extensions": { + "io.modelcontextprotocol/ui": {} + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize ClientCapabilities from JSON`() { + val json = """ + { + "sampling": {}, + "roots": { + "listChanged": true + }, + "elicitation": {} + } + """.trimIndent() + + val capabilities = verifyDeserialization(McpJson, json) + + assertEquals(ClientCapabilities.Sampling(), capabilities.sampling) + assertEquals(true, capabilities.roots?.listChanged) + assertEquals(ClientCapabilities.Elicitation(), capabilities.elicitation) + } + + @Test + fun `should deserialize empty ClientCapabilities from JSON`() { + val json = "{}" + + val capabilities = verifyDeserialization(McpJson, json) + + assertNull(capabilities.sampling) + assertNull(capabilities.roots) + assertNull(capabilities.elicitation) + assertNull(capabilities.experimental) + assertNull(capabilities.extensions) + } + + @Test + fun `should serialize and deserialize ClientCapabilities round trip`() { + val original = ClientCapabilities( + sampling = ClientCapabilities.Sampling(), + roots = ClientCapabilities.Roots(listChanged = false), + elicitation = ClientCapabilities.elicitation, + extensions = mapOf( + "io.modelcontextprotocol/ui" to buildJsonObject { + put("mimeTypes", buildJsonObject { put("text/html", true) }) + }, + ), + ) + + verifySerializationRoundTrip(original, McpJson) + } + + // ServerCapabilities tests + @Test + fun `should serialize empty ServerCapabilities`() { + val capabilities = ServerCapabilities() + val json = McpJson.encodeToString(capabilities) + + json shouldEqualJson "{}" + } + + @Test + fun `should serialize ServerCapabilities with tools without listChanged`() { + val capabilities = ServerCapabilities( + tools = ServerCapabilities.Tools(), + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "tools": {} + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ServerCapabilities with tools with listChanged`() { + val capabilities = ServerCapabilities( + tools = ServerCapabilities.Tools(listChanged = true), + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "tools": { + "listChanged": true + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ServerCapabilities with resources without options`() { + val capabilities = ServerCapabilities( + resources = ServerCapabilities.Resources(), + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "resources": {} + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ServerCapabilities with resources with listChanged`() { + val capabilities = ServerCapabilities( + resources = ServerCapabilities.Resources(listChanged = true), + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "resources": { + "listChanged": true + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ServerCapabilities with resources with subscribe`() { + val capabilities = ServerCapabilities( + resources = ServerCapabilities.Resources(subscribe = true), + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "resources": { + "subscribe": true + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ServerCapabilities with resources with both options`() { + val capabilities = ServerCapabilities( + resources = ServerCapabilities.Resources( + listChanged = true, + subscribe = false, + ), + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "resources": { + "listChanged": true, + "subscribe": false + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ServerCapabilities with prompts without listChanged`() { + val capabilities = ServerCapabilities( + prompts = ServerCapabilities.Prompts(), + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "prompts": {} + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ServerCapabilities with prompts with listChanged`() { + val capabilities = ServerCapabilities( + prompts = ServerCapabilities.Prompts(listChanged = false), + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "prompts": { + "listChanged": false + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ServerCapabilities with logging`() { + val capabilities = ServerCapabilities( + logging = ServerCapabilities.Logging, + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "logging": {} + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ServerCapabilities with completions`() { + val capabilities = ServerCapabilities( + completions = ServerCapabilities.Completions, + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "completions": {} + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ServerCapabilities with extensions`() { + val capabilities = ServerCapabilities( + extensions = mapOf( + "io.modelcontextprotocol/ui" to EmptyJsonObject, + "com.example/custom" to buildJsonObject { + put("setting", "value") + }, + ), + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "extensions": { + "io.modelcontextprotocol/ui": {}, + "com.example/custom": { + "setting": "value" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ServerCapabilities with experimental`() { + val experimental = buildJsonObject { + put( + "customCapability", + buildJsonObject { + put("version", "1.0") + }, + ) + } + val capabilities = ServerCapabilities( + experimental = experimental, + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "experimental": { + "customCapability": { + "version": "1.0" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ServerCapabilities with all fields`() { + val experimental = buildJsonObject { + put("feature1", buildJsonObject { put("enabled", true) }) + } + val extensions = mapOf( + "io.modelcontextprotocol/ui" to EmptyJsonObject, + ) + val capabilities = ServerCapabilities( + tools = ServerCapabilities.Tools(listChanged = true), + resources = ServerCapabilities.Resources( + listChanged = true, + subscribe = true, + ), + prompts = ServerCapabilities.Prompts(listChanged = false), + logging = ServerCapabilities.Logging, + completions = ServerCapabilities.Completions, + experimental = experimental, + extensions = extensions, + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "tools": { + "listChanged": true + }, + "resources": { + "listChanged": true, + "subscribe": true + }, + "prompts": { + "listChanged": false + }, + "logging": {}, + "completions": {}, + "experimental": { + "feature1": { + "enabled": true + } + }, + "extensions": { + "io.modelcontextprotocol/ui": {} + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize ServerCapabilities from JSON`() { + val json = """ + { + "tools": { + "listChanged": true + }, + "resources": { + "listChanged": false, + "subscribe": true + }, + "prompts": { + "listChanged": true + }, + "logging": {}, + "completions": {} + } + """.trimIndent() + + val capabilities = verifyDeserialization(McpJson, json) + + assertEquals(true, capabilities.tools?.listChanged) + assertEquals(false, capabilities.resources?.listChanged) + assertEquals(true, capabilities.resources?.subscribe) + assertEquals(true, capabilities.prompts?.listChanged) + assertEquals(EmptyJsonObject, capabilities.logging) + assertEquals(EmptyJsonObject, capabilities.completions) + } + + @Test + fun `should deserialize empty ServerCapabilities from JSON`() { + val json = "{}" + + val capabilities = verifyDeserialization(McpJson, json) + + assertNull(capabilities.tools) + assertNull(capabilities.resources) + assertNull(capabilities.prompts) + assertNull(capabilities.logging) + assertNull(capabilities.completions) + assertNull(capabilities.experimental) + assertNull(capabilities.extensions) + } + + @Test + fun `should serialize and deserialize ServerCapabilities round trip`() { + val original = ServerCapabilities( + tools = ServerCapabilities.Tools(listChanged = true), + resources = ServerCapabilities.Resources( + listChanged = false, + subscribe = true, + ), + prompts = ServerCapabilities.Prompts(listChanged = true), + logging = ServerCapabilities.Logging, + extensions = mapOf( + "io.modelcontextprotocol/ui" to EmptyJsonObject, + ), + ) + + verifySerializationRoundTrip(original, McpJson) + } + + // Additional nested type tests + @Test + fun `should deserialize ClientCapabilities Roots with null listChanged`() { + val json = """ + { + "roots": {} + } + """.trimIndent() + + val capabilities = verifyDeserialization(McpJson, json) + + assertNull(capabilities.roots?.listChanged) + } + + @Test + fun `should deserialize ServerCapabilities nested types with null values`() { + val json = """ + { + "tools": {}, + "resources": {}, + "prompts": {} + } + """.trimIndent() + + val capabilities = verifyDeserialization(McpJson, json) + + assertNull(capabilities.tools?.listChanged) + assertNull(capabilities.resources?.listChanged) + assertNull(capabilities.resources?.subscribe) + assertNull(capabilities.prompts?.listChanged) + } + + @Test + fun `should handle ClientCapabilities with additionalProperties in sampling`() { + val json = """ + { + "sampling": { + "customProperty": "customValue" + } + } + """.trimIndent() + + // Sampling is now a typed struct; unknown fields are ignored on deserialization + val capabilities = McpJson.decodeFromString(json) + + // Should not fail - additionalProperties are allowed (unknown fields are ignored by Sampling) + assertNotNull(capabilities.sampling) + } + + @Test + fun `should handle ServerCapabilities with additionalProperties in logging`() { + val json = """ + { + "logging": { + "level": "debug" + } + } + """.trimIndent() + + val capabilities = verifyDeserialization(McpJson, json) + + // Should not fail - additionalProperties are allowed + assertEquals("debug", capabilities.logging?.get("level")?.toString()?.trim('"')) + } + + // ============================================================================ + // ClientCapabilities.Sampling sub-capabilities (SEP-1577) + // ============================================================================ + + @Test + fun `empty sampling serialises as empty object`() { + val caps = ClientCapabilities(sampling = ClientCapabilities.Sampling()) + val json = McpJson.encodeToString(ClientCapabilities.serializer(), caps) + check("\"sampling\":{}" in json) { json } + } + + @Test + fun `sampling with tools sub-capability serialises tools`() { + val caps = ClientCapabilities(sampling = ClientCapabilities.Sampling(tools = EmptyJsonObject)) + val json = McpJson.encodeToString(ClientCapabilities.serializer(), caps) + check("\"tools\":{}" in json) { json } + } + + @Test + fun `sampling with context sub-capability serialises context`() { + val caps = ClientCapabilities(sampling = ClientCapabilities.Sampling(context = EmptyJsonObject)) + val json = McpJson.encodeToString(ClientCapabilities.serializer(), caps) + check("\"context\":{}" in json) { json } + } + + @Test + fun `sampling with combined tools and context round-trips`() { + val original = ClientCapabilities( + sampling = ClientCapabilities.Sampling( + context = EmptyJsonObject, + tools = EmptyJsonObject, + ), + ) + val json = McpJson.encodeToString(ClientCapabilities.serializer(), original) + val decoded = McpJson.decodeFromString(ClientCapabilities.serializer(), json) + decoded shouldBe original + decoded.sampling?.tools shouldBe EmptyJsonObject + decoded.sampling?.context shouldBe EmptyJsonObject + } + + @Test + fun `absent sampling means unsupported`() { + val caps = ClientCapabilities() + caps.sampling shouldBe null + } + + @Test + fun `should serialize ServerCapabilities with tasks`() { + val capabilities = ServerCapabilities( + tasks = ServerCapabilities.Tasks( + list = EmptyJsonObject, + cancel = EmptyJsonObject, + requests = ServerCapabilities.Tasks.Requests( + tools = ServerCapabilities.Tasks.Requests.Tools(call = EmptyJsonObject), + ), + ), + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "tasks": { + "list": {}, + "cancel": {}, + "requests": { + "tools": { + "call": {} + } + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize ServerCapabilities with tasks`() { + val json = """ + { + "tasks": { + "list": {}, + "requests": {"tools": {"call": {}}} + } + } + """.trimIndent() + val capabilities = verifyDeserialization(McpJson, json) + assertEquals(EmptyJsonObject, capabilities.tasks?.list) + assertNull(capabilities.tasks?.cancel) + assertEquals(EmptyJsonObject, capabilities.tasks?.requests?.tools?.call) + } + + @Test + fun `should serialize ClientCapabilities with tasks`() { + val capabilities = ClientCapabilities( + tasks = ClientCapabilities.Tasks( + list = EmptyJsonObject, + cancel = EmptyJsonObject, + requests = ClientCapabilities.Tasks.Requests( + sampling = ClientCapabilities.Tasks.Requests.Sampling(createMessage = EmptyJsonObject), + elicitation = ClientCapabilities.Tasks.Requests.Elicitation(create = EmptyJsonObject), + ), + ), + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "tasks": { + "list": {}, + "cancel": {}, + "requests": { + "sampling": {"createMessage": {}}, + "elicitation": {"create": {}} + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ClientCapabilities with structured elicitation form and url`() { + val capabilities = ClientCapabilities( + elicitation = ClientCapabilities.Elicitation(form = EmptyJsonObject, url = EmptyJsonObject), + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "elicitation": { + "form": {}, + "url": {} + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize ClientCapabilities with elicitation and tasks`() { + val json = """ + { + "elicitation": {"form": {}, "url": {}}, + "tasks": { + "list": {}, + "cancel": {}, + "requests": { + "sampling": {"createMessage": {}}, + "elicitation": {"create": {}} + } + } + } + """.trimIndent() + val capabilities = verifyDeserialization(McpJson, json) + assertEquals(EmptyJsonObject, capabilities.elicitation?.form) + assertEquals(EmptyJsonObject, capabilities.elicitation?.url) + assertEquals(EmptyJsonObject, capabilities.tasks?.list) + assertEquals(EmptyJsonObject, capabilities.tasks?.cancel) + assertEquals(EmptyJsonObject, capabilities.tasks?.requests?.sampling?.createMessage) + assertEquals(EmptyJsonObject, capabilities.tasks?.requests?.elicitation?.create) + } + + // ── Elicitation supportsUrl predicate ─────────────────────────────── + + @Test + fun `supportsUrl is false when no elicitation capability is declared`() { + val elicitation: ClientCapabilities.Elicitation? = null + elicitation.supportsUrl shouldBe false + } + + @Test + fun `supportsUrl is false for an empty form-only capability`() { + ClientCapabilities.Elicitation().supportsUrl shouldBe false + ClientCapabilities.Elicitation(form = EmptyJsonObject).supportsUrl shouldBe false + } + + @Test + fun `supportsUrl is true when url mode is declared`() { + ClientCapabilities.Elicitation(url = EmptyJsonObject).supportsUrl shouldBe true + ClientCapabilities.Elicitation(form = EmptyJsonObject, url = EmptyJsonObject).supportsUrl shouldBe true + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CommonTypeTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CommonTypeTest.kt new file mode 100644 index 0000000..a29a19c --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CommonTypeTest.kt @@ -0,0 +1,190 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerializationRoundTrip +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class CommonTypeTest { + + @Test + fun `should have correct latest protocol version`() { + LATEST_PROTOCOL_VERSION shouldBe "2025-11-25" + } + + @Test + fun `should have correct supported protocol versions`() { + SUPPORTED_PROTOCOL_VERSIONS shouldContainExactlyInAnyOrder listOf( + LATEST_PROTOCOL_VERSION, + "2025-06-18", + "2025-03-26", + "2024-11-05", + ) + } + + @Test + fun `should serialize Icon with minimal fields`() { + val icon = Icon(src = "https://example.com/icon.png") + verifySerialization( + icon, + McpJson, + """ + { + "src": "https://example.com/icon.png" + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize Icon with all fields`() { + val icon = Icon( + src = "https://example.com/icon.png", + mimeType = "image/png", + sizes = listOf("48x48", "96x96"), + theme = Icon.Theme.Light, + ) + verifySerialization( + icon, + McpJson, + """ + { + "src": "https://example.com/icon.png", + "mimeType": "image/png", + "sizes": ["48x48", "96x96"], + "theme": "light" + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize Icon from JSON`() { + val json = """ + { + "src": "https://example.com/icon.png", + "mimeType": "image/png", + "sizes": ["48x48"], + "theme": "dark" + } + """.trimIndent() + + val icon = verifyDeserialization(McpJson, json) + + assertEquals("https://example.com/icon.png", icon.src) + assertEquals("image/png", icon.mimeType) + assertEquals(listOf("48x48"), icon.sizes) + assertEquals(Icon.Theme.Dark, icon.theme) + } + + @Test + fun `should deserialize Icon with minimal fields from JSON`() { + val json = """{"src": "https://example.com/icon.png"}""" + + val icon = verifyDeserialization(McpJson, json) + + assertEquals("https://example.com/icon.png", icon.src) + assertNull(icon.mimeType) + assertNull(icon.sizes) + assertNull(icon.theme) + } + + @Test + fun `should serialize theme as lowercase strings`() { + val lightIcon = Icon(src = "test.png", theme = Icon.Theme.Light) + val darkIcon = Icon(src = "test.png", theme = Icon.Theme.Dark) + + val lightJson = McpJson.encodeToString(lightIcon) + val darkJson = McpJson.encodeToString(darkIcon) + + assertTrue(lightJson.contains("\"theme\":\"light\"")) + assertTrue(darkJson.contains("\"theme\":\"dark\"")) + } + + @Test + fun `should serialize Role as lowercase strings`() { + val userJson = McpJson.encodeToString(Role.User) + val assistantJson = McpJson.encodeToString(Role.Assistant) + + assertEquals("\"user\"", userJson) + assertEquals("\"assistant\"", assistantJson) + } + + @Test + fun `should deserialize Role from lowercase strings`() { + val user = verifyDeserialization(McpJson, "\"user\"") + val assistant = verifyDeserialization(McpJson, "\"assistant\"") + + assertEquals(Role.User, user) + assertEquals(Role.Assistant, assistant) + } + + @Test + fun `should serialize Annotations with all fields`() { + val annotations = Annotations( + audience = listOf(Role.User, Role.Assistant), + priority = 0.8, + lastModified = "2025-01-12T15:00:58Z", + ) + + verifySerialization( + annotations, + McpJson, + """ + { + "audience": ["user", "assistant"], + "priority": 0.8, + "lastModified": "2025-01-12T15:00:58Z" + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize Annotations with minimal fields`() { + val annotations = Annotations(priority = 0.5) + + verifySerialization( + annotations, + McpJson, + """ + { + "priority": 0.5 + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize Annotations from JSON`() { + val json = """ + { + "audience": ["user"], + "priority": 0.9, + "lastModified": "2025-01-12T15:00:58Z" + } + """.trimIndent() + + val annotations = verifyDeserialization(McpJson, json) + + assertEquals(listOf(Role.User), annotations.audience) + assertEquals(0.9, annotations.priority) + assertEquals("2025-01-12T15:00:58Z", annotations.lastModified) + } + + @Test + fun `should serialize and deserialize Annotations round trip`() { + val original = Annotations( + audience = listOf(Role.User, Role.Assistant), + priority = 0.42, + lastModified = "2025-01-12T15:00:58Z", + ) + + verifySerializationRoundTrip(original, McpJson) + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CompletionTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CompletionTest.kt new file mode 100644 index 0000000..03a76e4 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CompletionTest.kt @@ -0,0 +1,235 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.kotest.assertions.json.shouldEqualJson +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertIs +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class CompletionTest { + + @Test + fun `should serialize CompleteRequest with minimal fields`() { + val request = CompleteRequest( + CompleteRequestParams( + argument = CompleteRequestParams.Argument( + name = "name", + value = "A", + ), + ref = PromptReference(name = "greeting"), + ), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "completion/complete", + "params": { + "argument": { + "name": "name", + "value": "A" + }, + "ref": { + "type": "ref/prompt", + "name": "greeting" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize CompleteRequest with context and meta`() { + val request = CompleteRequest( + CompleteRequestParams( + argument = CompleteRequestParams.Argument( + name = "repo", + value = "mcp", + ), + ref = ResourceTemplateReference(uri = "github://repos/{owner}/{repo}"), + context = CompleteRequestParams.Context( + arguments = mapOf( + "owner" to "modelcontextprotocol", + "language" to "kotlin", + ), + ), + meta = RequestMeta( + buildJsonObject { + put("progressToken", "token-123") + }, + ), + ), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "completion/complete", + "params": { + "argument": { + "name": "repo", + "value": "mcp" + }, + "ref": { + "type": "ref/resource", + "uri": "github://repos/{owner}/{repo}" + }, + "context": { + "arguments": { + "owner": "modelcontextprotocol", + "language": "kotlin" + } + }, + "_meta": { + "progressToken": "token-123" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize CompleteRequest from JSON`() { + val json = """ + { + "method": "completion/complete", + "params": { + "argument": { + "name": "file", + "value": "mai" + }, + "ref": { + "type": "ref/resource", + "uri": "file:///{path}" + }, + "context": { + "arguments": { + "path": "src/main" + } + }, + "_meta": { + "progressToken": 42 + } + } + } + """.trimIndent() + + val request = verifyDeserialization(McpJson, json) + assertEquals(Method.Defined.CompletionComplete, request.method) + + val params = request.params + assertEquals("file", params.argument.name) + assertEquals("mai", params.argument.value) + val ref = params.ref + assertIs(ref) + assertEquals("file:///{path}", ref.uri) + assertNotNull(params.context) + assertEquals(mapOf("path" to "src/main"), params.context.arguments) + assertEquals(ProgressToken(42), params.meta?.progressToken) + } + + @Test + fun `should serialize CompleteResult with all fields`() { + val result = CompleteResult( + completion = CompleteResult.Completion( + values = listOf("src/main/kotlin/App.kt", "src/main/kotlin/Main.kt"), + total = 25, + hasMore = true, + ), + meta = buildJsonObject { + put("source", "cache") + }, + ) + + verifySerialization( + result, + McpJson, + """ + { + "completion": { + "values": [ + "src/main/kotlin/App.kt", + "src/main/kotlin/Main.kt" + ], + "total": 25, + "hasMore": true + }, + "_meta": { + "source": "cache" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize CompleteResult from JSON`() { + val json = """ + { + "completion": { + "values": ["README.md", "CONTRIBUTING.md"], + "total": 2, + "hasMore": false + }, + "_meta": { + "fetchedAt": "2025-01-12T15:00:58Z" + } + } + """.trimIndent() + + val result = verifyDeserialization(McpJson, json) + assertEquals(listOf("README.md", "CONTRIBUTING.md"), result.completion.values) + assertEquals(2, result.completion.total) + assertEquals(false, result.completion.hasMore) + assertNotNull(result.meta) + assertEquals( + "2025-01-12T15:00:58Z", + result.meta["fetchedAt"]?.jsonPrimitive?.content, + ) + } + + @Test + fun `should allow omitting optional Total and HasMore`() { + val result = CompleteResult( + completion = CompleteResult.Completion(values = listOf("foo")), + meta = null, + ) + + val json = McpJson.encodeToString(result) + json shouldEqualJson """ + { + "completion": { + "values": ["foo"] + } + } + """.trimIndent() + + val decoded = verifyDeserialization(McpJson, json) + assertEquals(listOf("foo"), decoded.completion.values) + assertNull(decoded.completion.total) + assertNull(decoded.completion.hasMore) + assertNull(decoded.meta) + } + + @Test + fun `should enforce maximum of 100 completion values`() { + val values = (1..101).map { "entry-$it" } + + assertFailsWith { + CompleteResult.Completion(values = values) + } + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ContentTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ContentTest.kt new file mode 100644 index 0000000..c6dee5d --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ContentTest.kt @@ -0,0 +1,449 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization +import kotlinx.serialization.SerializationException +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertIs +import kotlin.test.assertNull + +class ContentTest { + + private val samplingContentSerializer = ListSerializer(SamplingMessageContent.serializer()) + + @Test + fun `should serialize TextContent with minimal fields`() { + val content = TextContent(text = "Hello, MCP!") + + verifySerialization( + content, + McpJson, + """ + { + "type": "text", + "text": "Hello, MCP!" + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize and deserialize TextContent with annotations`() { + val content = TextContent( + text = "Need help?", + annotations = Annotations(audience = listOf(Role.User)), + meta = buildJsonObject { put("origin", "assistant") }, + ) + + verifySerialization( + content, + McpJson, + """ + { + "type": "text", + "text": "Need help?", + "annotations": { + "audience": ["user"] + }, + "_meta": { + "origin": "assistant" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ImageContent`() { + val content = ImageContent( + data = "aW1hZ2VEYXRh", + mimeType = "image/png", + ) + + verifySerialization( + content, + McpJson, + """ + { + "type": "image", + "data": "aW1hZ2VEYXRh", + "mimeType": "image/png" + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize ImageContent from JSON`() { + val json = """ + { + "type": "image", + "data": "Zm9vYmFy", + "mimeType": "image/jpeg", + "annotations": {"priority": 0.6} + } + """.trimIndent() + + val content = verifyDeserialization(McpJson, json) as ImageContent + assertEquals("Zm9vYmFy", content.data) + assertEquals("image/jpeg", content.mimeType) + assertEquals(0.6, content.annotations?.priority) + assertNull(content.meta) + } + + @Test + fun `should serialize AudioContent`() { + val content = AudioContent( + data = "YXVkaW9kYXRh", + mimeType = "audio/wav", + meta = buildJsonObject { put("durationMs", 1200) }, + ) + + verifySerialization( + content, + McpJson, + """ + { + "type": "audio", + "data": "YXVkaW9kYXRh", + "mimeType": "audio/wav", + "_meta": { + "durationMs": 1200 + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize AudioContent from JSON`() { + val json = """ + { + "type": "audio", + "data": "YmF6", + "mimeType": "audio/mpeg", + "_meta": {"speaker": "user"} + } + """.trimIndent() + + val content = verifyDeserialization(McpJson, json) as AudioContent + assertEquals("audio/mpeg", content.mimeType) + assertEquals("YmF6", content.data) + assertEquals("user", content.meta?.get("speaker")?.jsonPrimitive?.content) + } + + @Test + fun `should serialize ResourceLink with minimal fields`() { + val resource = ResourceLink( + name = "README", + uri = "file:///workspace/README.md", + ) + + verifySerialization( + resource, + McpJson, + """ + { + "type": "resource_link", + "name": "README", + "uri": "file:///workspace/README.md" + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ResourceLink with all fields`() { + val resource = ResourceLink( + name = "README", + uri = "file:///workspace/README.md", + title = "Workspace README", + size = 2048, + mimeType = "text/markdown", + description = "Primary documentation", + icons = listOf(Icon(src = "https://example.com/icon.png")), + annotations = Annotations(priority = 0.75), + meta = buildJsonObject { put("etag", "1234") }, + ) + + verifySerialization( + resource, + McpJson, + """ + { + "type": "resource_link", + "name": "README", + "uri": "file:///workspace/README.md", + "title": "Workspace README", + "size": 2048, + "mimeType": "text/markdown", + "description": "Primary documentation", + "icons": [ + { + "src": "https://example.com/icon.png" + } + ], + "annotations": { + "priority": 0.75 + }, + "_meta": { + "etag": "1234" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize ResourceLink from JSON`() { + val json = """ + { + "type": "resource_link", + "name": "config", + "uri": "file:///config.yaml", + "size": 512, + "annotations": { "lastModified": "2025-01-12T15:00:58Z" } + } + """.trimIndent() + + val resource = verifyDeserialization(McpJson, json) as ResourceLink + assertEquals("config", resource.name) + assertEquals("file:///config.yaml", resource.uri) + assertEquals(512, resource.size) + assertEquals("2025-01-12T15:00:58Z", resource.annotations?.lastModified) + assertNull(resource.meta) + } + + @Test + fun `should serialize EmbeddedResource with text contents`() { + val embedded = EmbeddedResource( + resource = TextResourceContents( + text = "fun main() = println(\"Hello\")", + uri = "file:///workspace/Main.kt", + mimeType = "text/x-kotlin", + meta = buildJsonObject { put("languageId", "kotlin") }, + ), + annotations = Annotations(audience = listOf(Role.Assistant)), + meta = buildJsonObject { put("source", "analysis") }, + ) + + verifySerialization( + embedded, + McpJson, + """ + { + "type": "resource", + "resource": { + "text": "fun main() = println(\"Hello\")", + "uri": "file:///workspace/Main.kt", + "mimeType": "text/x-kotlin", + "_meta": { + "languageId": "kotlin" + } + }, + "annotations": { + "audience": ["assistant"] + }, + "_meta": { + "source": "analysis" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize EmbeddedResource with blob contents`() { + val json = """ + { + "type": "resource", + "resource": { + "blob": "YmFzZTY0", + "uri": "file:///workspace/archive.bin", + "mimeType": "application/octet-stream" + }, + "_meta": {"encoding": "base64"} + } + """.trimIndent() + + val embedded = verifyDeserialization(McpJson, json) as EmbeddedResource + assertEquals(ContentTypes.EMBEDDED_RESOURCE, embedded.type) + assertNull(embedded.annotations) + assertEquals("base64", embedded.meta?.get("encoding")?.jsonPrimitive?.content) + + val resource = embedded.resource + assertIs(resource) + assertEquals("YmFzZTY0", resource.blob) + assertEquals("file:///workspace/archive.bin", resource.uri) + assertEquals("application/octet-stream", resource.mimeType) + } + + @Test + fun `should decode heterogeneous content blocks`() { + val json = """ + [ + { + "type": "text", + "text": "Hello" + }, + { + "type": "resource_link", + "name": "log", + "uri": "file:///tmp/log.txt" + }, + { + "type": "resource", + "resource": { + "text": "line1", + "uri": "file:///tmp/log.txt" + } + } + ] + """.trimIndent() + + val content = verifyDeserialization>(McpJson, json) + assertEquals(3, content.size) + assertIs(content[0]) + assertIs(content[1]) + assertIs(content[2]) + } + + // ============================================================================ + // SamplingMessageContent hierarchy (SEP-1577) + // ============================================================================ + + @Test + fun `TextContent implements SamplingMessageContent`() { + val c: SamplingMessageContent = TextContent("hi") + c.shouldBeInstanceOf() + } + + @Test + fun `ImageContent implements SamplingMessageContent`() { + val c: SamplingMessageContent = ImageContent(data = "AA==", mimeType = "image/png") + c.shouldBeInstanceOf() + } + + @Test + fun `AudioContent implements SamplingMessageContent`() { + val c: SamplingMessageContent = AudioContent(data = "AA==", mimeType = "audio/wav") + c.shouldBeInstanceOf() + } + + // ============================================================================ + // ToolUseContent + // ============================================================================ + + @Test + fun `ToolUseContent round-trips through McpJson`() { + val input = buildJsonObject { put("location", JsonPrimitive("London")) } + val original = ToolUseContent(id = "call_1", name = "get_weather", input = input) + val encoded = McpJson.encodeToString(ToolUseContent.serializer(), original) + val decoded = McpJson.decodeFromString(ToolUseContent.serializer(), encoded) + decoded shouldBe original + } + + @Test + fun `ToolUseContent serialises with type discriminator tool_use`() { + val c = ToolUseContent(id = "x", name = "n", input = JsonObject(emptyMap())) + val json = McpJson.encodeToString(ToolUseContent.serializer(), c) + check("\"type\":\"tool_use\"" in json) { "missing discriminator in: $json" } + } + + @Test + fun `ToolUseContent preserves meta when present`() { + val meta = buildJsonObject { put("cacheControl", JsonPrimitive("ephemeral")) } + val c = ToolUseContent(id = "x", name = "n", input = JsonObject(emptyMap()), meta = meta) + val json = McpJson.encodeToString(ToolUseContent.serializer(), c) + val decoded = McpJson.decodeFromString(ToolUseContent.serializer(), json) + decoded.meta shouldBe meta + } + + // ============================================================================ + // ToolResultContent + // ============================================================================ + + @Test + fun `ToolResultContent round-trips with text content block`() { + val original = ToolResultContent( + toolUseId = "call_1", + content = listOf(TextContent("20°C sunny")), + ) + val encoded = McpJson.encodeToString(ToolResultContent.serializer(), original) + val decoded = McpJson.decodeFromString(ToolResultContent.serializer(), encoded) + decoded shouldBe original + } + + @Test + fun `ToolResultContent round-trips with mixed content including resource_link`() { + val original = ToolResultContent( + toolUseId = "call_2", + content = listOf( + TextContent("see file"), + ResourceLink(name = "log", uri = "file:///tmp/a.log"), + ), + ) + val encoded = McpJson.encodeToString(ToolResultContent.serializer(), original) + val decoded = McpJson.decodeFromString(ToolResultContent.serializer(), encoded) + decoded shouldBe original + } + + @Test + fun `ToolResultContent serialises with discriminator and preserves structuredContent and isError`() { + val original = ToolResultContent( + toolUseId = "call_3", + content = emptyList(), + structuredContent = buildJsonObject { put("temp", JsonPrimitive(20)) }, + isError = true, + ) + val json = McpJson.encodeToString(ToolResultContent.serializer(), original) + check("\"type\":\"tool_result\"" in json) { "missing discriminator in: $json" } + val decoded = McpJson.decodeFromString(ToolResultContent.serializer(), json) + decoded shouldBe original + } + + // ============================================================================ + // SamplingMessageContentPolymorphicSerializer + // ============================================================================ + + @Test + fun `SamplingMessageContent polymorphic serializer decodes text`() { + val json = """[{"type":"text","text":"hi"}]""" + val list = McpJson.decodeFromString(samplingContentSerializer, json) + list[0].shouldBeInstanceOf().text shouldBe "hi" + } + + @Test + fun `SamplingMessageContent polymorphic serializer decodes tool_use`() { + val json = """[{"type":"tool_use","id":"a","name":"n","input":{}}]""" + val list = McpJson.decodeFromString(samplingContentSerializer, json) + val use = list[0].shouldBeInstanceOf() + use.id shouldBe "a" + use.name shouldBe "n" + } + + @Test + fun `SamplingMessageContent polymorphic serializer decodes tool_result`() { + val json = """[{"type":"tool_result","toolUseId":"a","content":[]}]""" + val list = McpJson.decodeFromString(samplingContentSerializer, json) + list[0].shouldBeInstanceOf().toolUseId shouldBe "a" + } + + @Test + fun `SamplingMessageContent polymorphic serializer throws on unknown discriminator`() { + val json = """[{"type":"bogus","x":1}]""" + assertFailsWith { + McpJson.decodeFromString(samplingContentSerializer, json) + } + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/DiscoveryTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/DiscoveryTest.kt new file mode 100644 index 0000000..691af45 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/DiscoveryTest.kt @@ -0,0 +1,324 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.kotest.assertions.json.shouldEqualJson +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFails +import kotlin.test.assertFailsWith +import kotlin.test.assertIs +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +@OptIn(ExperimentalMcpApi::class) +class DiscoveryTest { + + @Test + fun `should decode server discover request from its wire shape`() { + val request = McpJson.decodeFromString( + """ + { + "method": "server/discover", + "params": { + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": { + "name": "test-client", + "version": "1.0.0" + }, + "io.modelcontextprotocol/clientCapabilities": {} + } + } + } + """.trimIndent(), + ) + + val discover = assertIs(request) + assertEquals(Method.Defined.ServerDiscover, discover.method) + assertEquals("2026-07-28", discover.meta.protocolVersion) + assertEquals("test-client", discover.meta.clientInfo?.name) + assertEquals(ClientCapabilities(), discover.meta.clientCapabilities) + } + + @Test + fun `should decode discovery request without optional client info`() { + val request = McpJson.decodeFromString( + """ + { + "method": "server/discover", + "params": { + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {}, + "com.example/traceId": "trace-123" + } + } + } + """.trimIndent(), + ) + + val discover = assertIs(request) + assertEquals("2026-07-28", discover.meta.protocolVersion) + assertEquals(ClientCapabilities(), discover.meta.clientCapabilities) + assertNull(discover.meta.clientInfo) + assertNotNull(discover.meta["com.example/traceId"]) + McpJson.encodeToString(discover) shouldEqualJson """ + { + "method": "server/discover", + "params": { + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {}, + "com.example/traceId": "trace-123" + } + } + } + """.trimIndent() + } + + @Test + fun `should reject discovery requests without required request metadata`() { + val invalidParams = listOf( + "{}" to "_meta", + """{"_meta": {}}""" to RequestMetaKeys.PROTOCOL_VERSION, + """ + { + "_meta": { + "${RequestMetaKeys.PROTOCOL_VERSION}": "2026-07-28", + "${RequestMetaKeys.CLIENT_INFO}": { + "name": "test-client", + "version": "1.0.0" + } + } + } + """.trimIndent() to RequestMetaKeys.CLIENT_CAPABILITIES, + ) + + invalidParams.forEach { (params, missingField) -> + val failure = assertFails { + McpJson.decodeFromString( + """{"method": "server/discover", "params": $params}""", + ) + } + + assertTrue( + failure.message.orEmpty().contains(missingField), + "Expected failure to identify missing field $missingField, but was: $failure", + ) + } + } + + @Test + fun `should serialize every required discovery result field`() { + val result = DiscoverResult( + supportedVersions = listOf("2026-07-28"), + capabilities = ServerCapabilities( + tools = ServerCapabilities.Tools(listChanged = true), + ), + instructions = "Use tools intentionally.", + ) + + McpJson.encodeToString(result) shouldEqualJson """ + { + "supportedVersions": ["2026-07-28"], + "capabilities": { + "tools": {"listChanged": true} + }, + "instructions": "Use tools intentionally.", + "resultType": "complete", + "ttlMs": 0, + "cacheScope": "private" + } + """.trimIndent() + } + + @Test + fun `should encode optional server info in result metadata`() { + val result = DiscoverResult( + supportedVersions = listOf("2026-07-28"), + capabilities = ServerCapabilities(), + meta = Json.parseToJsonElement( + """ + { + "io.modelcontextprotocol/serverInfo": { + "name": "test-server", + "version": "2.0.0" + }, + "com.example/source": "edge" + } + """.trimIndent(), + ).jsonObject, + ) + + McpJson.encodeToString(result) shouldEqualJson """ + { + "supportedVersions": ["2026-07-28"], + "capabilities": {}, + "resultType": "complete", + "ttlMs": 0, + "cacheScope": "private", + "_meta": { + "io.modelcontextprotocol/serverInfo": { + "name": "test-server", + "version": "2.0.0" + }, + "com.example/source": "edge" + } + } + """.trimIndent() + assertEquals( + Json.parseToJsonElement("""{"name":"test-server","version":"2.0.0"}"""), + result.meta?.get("io.modelcontextprotocol/serverInfo"), + ) + } + + @Test + fun `should encode required defaults independently of McpJson`() { + val result = DiscoverResult( + supportedVersions = listOf("2026-07-28"), + capabilities = ServerCapabilities(), + ) + + Json.encodeToString(result) shouldEqualJson """ + { + "supportedVersions": ["2026-07-28"], + "capabilities": {}, + "resultType": "complete", + "ttlMs": 0, + "cacheScope": "private" + } + """.trimIndent() + } + + @Test + fun `should reject discovery results without required cache fields`() { + val invalidResults = listOf( + """ + { + "supportedVersions": ["2026-07-28"], + "capabilities": {}, + "cacheScope": "private" + } + """.trimIndent(), + """ + { + "supportedVersions": ["2026-07-28"], + "capabilities": {}, + "ttlMs": 0 + } + """.trimIndent(), + ) + + invalidResults.forEach { wire -> + assertFails { McpJson.decodeFromString(wire) } + assertFails { McpJson.decodeFromString(wire) } + } + } + + @Test + fun `should use complete when a discovery result omits resultType`() { + val result = McpJson.decodeFromString( + """ + { + "supportedVersions": ["2026-07-28"], + "capabilities": {}, + "ttlMs": 0, + "cacheScope": "private" + } + """.trimIndent(), + ) + + assertEquals(COMPLETE_RESULT_TYPE, assertIs(result).resultType) + } + + @Test + fun `should deserialize discovery results polymorphically`() { + val wire = + """ + { + "supportedVersions": ["2026-07-28"], + "capabilities": {}, + "resultType": "complete", + "ttlMs": 250, + "cacheScope": "public", + "_meta": { + "io.modelcontextprotocol/serverInfo": { + "name": "server", + "version": "1" + }, + "com.example/source": "edge" + } + } + """.trimIndent() + val result = McpJson.decodeFromString(wire) + + val discover = assertIs(result) + assertEquals(250, discover.ttlMs) + assertEquals(CacheScope.Public, discover.cacheScope) + assertEquals( + Json.parseToJsonElement("""{"name":"server","version":"1"}"""), + discover.meta?.get("io.modelcontextprotocol/serverInfo"), + ) + assertNotNull(discover.meta?.get("com.example/source")) + McpJson.encodeToString(discover) shouldEqualJson wire + } + + @Test + fun `should not treat supportedVersions alone as a discovery discriminator`() { + val result = McpJson.decodeFromString( + """ + { + "tools": [], + "supportedVersions": ["extension-value"] + } + """.trimIndent(), + ) + + assertIs(result) + } + + @Test + fun `should not treat an initialize extension as a discovery result`() { + val result = McpJson.decodeFromString( + """ + { + "protocolVersion": "2025-06-18", + "capabilities": {}, + "serverInfo": { + "name": "test-server", + "version": "1.0.0" + }, + "supportedVersions": ["extension-value"] + } + """.trimIndent(), + ) + + assertIs(result) + } + + @Test + fun `should reject a negative discovery ttl`() { + assertFailsWith { + DiscoverResult( + supportedVersions = listOf("2026-07-28"), + capabilities = ServerCapabilities(), + ttlMs = -1, + ) + } + } + + @Test + fun `unsupported version data should round trip without losing versions`() { + val data = UnsupportedProtocolVersionData( + supported = listOf("2026-07-28", "2025-11-25"), + requested = "2099-01-01", + ) + + val encoded = McpJson.encodeToString(data) + assertEquals(data, McpJson.decodeFromString(encoded)) + assertEquals(-32022, RPCError.ErrorCode.UNSUPPORTED_PROTOCOL_VERSION) + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ElicitationTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ElicitationTest.kt new file mode 100644 index 0000000..bd5ef06 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ElicitationTest.kt @@ -0,0 +1,359 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.kotest.assertions.json.shouldEqualJson +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.encodeToJsonElement +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.test.Test + +class ElicitationTest { + + // ── Form mode ─────────────────────────────────────────────────────── + + @Test + fun `should serialize form mode ElicitRequest`() { + val request = ElicitRequest( + ElicitRequestFormParams( + message = "Provide repository details", + requestedSchema = ElicitRequestParams.RequestedSchema( + properties = mapOf( + "owner" to StringSchema(description = "GitHub organization"), + "repository" to StringSchema(title = "Repository name"), + "private" to BooleanSchema(description = "Is the repository private?"), + ), + required = listOf("owner", "repository"), + ), + meta = RequestMeta( + buildJsonObject { put("progressToken", "token-42") }, + ), + ), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "elicitation/create", + "params": { + "message": "Provide repository details", + "requestedSchema": { + "properties": { + "owner": { + "description": "GitHub organization", + "type": "string" + }, + "repository": { + "title": "Repository name", + "type": "string" + }, + "private": { + "description": "Is the repository private?", + "type": "boolean" + } + }, + "required": ["owner", "repository"], + "type": "object" + }, + "_meta": { + "progressToken": "token-42" + }, + "mode": "form" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize form mode ElicitRequest with explicit mode`() { + val json = """ + { + "method": "elicitation/create", + "params": { + "mode": "form", + "message": "Enter your name", + "requestedSchema": { + "properties": { + "name": {"type": "string", "title": "Full name"} + }, + "type": "object" + } + } + } + """.trimIndent() + + val request = verifyDeserialization(McpJson, json) + request.method shouldBe Method.Defined.ElicitationCreate + request.message shouldBe "Enter your name" + + val params = request.params.shouldBeInstanceOf() + val nameSchema = params.requestedSchema.properties["name"].shouldBeInstanceOf() + nameSchema.title shouldBe "Full name" + } + + @Test + fun `should deserialize form mode ElicitRequest without mode field`() { + val json = """ + { + "method": "elicitation/create", + "params": { + "message": "Enter your name", + "requestedSchema": { + "properties": { + "name": {"type": "string"} + }, + "type": "object" + } + } + } + """.trimIndent() + + val request = McpJson.decodeFromString(json) + request.params.shouldBeInstanceOf() + request.params.message shouldBe "Enter your name" + } + + // ── URL mode ──────────────────────────────────────────────────────── + + @Test + fun `should serialize URL mode ElicitRequest`() { + val request = ElicitRequest( + ElicitRequestURLParams( + message = "Please provide your API key", + elicitationId = "550e8400-e29b-41d4-a716-446655440000", + url = "https://mcp.example.com/ui/set_api_key", + ), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "elicitation/create", + "params": { + "message": "Please provide your API key", + "elicitationId": "550e8400-e29b-41d4-a716-446655440000", + "url": "https://mcp.example.com/ui/set_api_key", + "mode": "url" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize URL mode ElicitRequest`() { + val json = """ + { + "method": "elicitation/create", + "params": { + "mode": "url", + "message": "Authorize access", + "elicitationId": "id-123", + "url": "https://example.com/auth" + } + } + """.trimIndent() + + val request = verifyDeserialization(McpJson, json) + val params = request.params.shouldBeInstanceOf() + params.message shouldBe "Authorize access" + params.elicitationId shouldBe "id-123" + params.url shouldBe "https://example.com/auth" + } + + // ── Deprecated compat ─────────────────────────────────────────────── + + @Test + fun `deprecated factory function should create ElicitRequestFormParams`() { + val schema = ElicitRequestParams.RequestedSchema( + properties = mapOf("name" to StringSchema()), + ) + val params = ElicitRequestParams(message = "Test", requestedSchema = schema) + params.shouldBeInstanceOf() + params.message shouldBe "Test" + params.requestedSchema shouldBe schema + } + + @Test + fun `deprecated requestedSchema should return schema for form mode`() { + val schema = ElicitRequestParams.RequestedSchema( + properties = mapOf("x" to BooleanSchema()), + ) + val request = ElicitRequest(ElicitRequestFormParams(message = "m", requestedSchema = schema)) + request.requestedSchema shouldBe schema + } + + @Test + fun `deprecated requestedSchema should return null for URL mode`() { + val request = ElicitRequest( + ElicitRequestURLParams(message = "m", elicitationId = "id", url = "https://example.com"), + ) + request.requestedSchema.shouldBeNull() + } + + // ── ElicitResult ──────────────────────────────────────────────────── + + @Test + fun `should serialize and deserialize accept result with content`() { + val result = ElicitResult( + action = ElicitResult.Action.Accept, + content = buildJsonObject { + put("repository", "kotlin-sdk") + put("stars", 128) + put("private", false) + }, + meta = buildJsonObject { + put("submittedAt", "2025-01-12T15:00:58Z") + }, + ) + + val json = McpJson.encodeToString(result) + + json shouldEqualJson """ + { + "action": "accept", + "content": { + "repository": "kotlin-sdk", + "stars": 128, + "private": false + }, + "_meta": { + "submittedAt": "2025-01-12T15:00:58Z" + } + } + """.trimIndent() + + val decoded = verifyDeserialization(McpJson, json) + decoded.action shouldBe ElicitResult.Action.Accept + + val content = decoded.content.shouldNotBeNull() + content["repository"]?.jsonPrimitive?.content shouldBe "kotlin-sdk" + content["stars"]?.jsonPrimitive?.int shouldBe 128 + content["private"]?.jsonPrimitive?.boolean shouldBe false + + val meta = decoded.meta.shouldNotBeNull() + meta["submittedAt"]?.jsonPrimitive?.content shouldBe "2025-01-12T15:00:58Z" + } + + @Test + fun `should deserialize decline result without content`() { + val json = """ + { + "action": "decline", + "_meta": { + "reason": "User skipped" + } + } + """.trimIndent() + + val result = verifyDeserialization(McpJson, json) + result.action shouldBe ElicitResult.Action.Decline + result.content.shouldBeNull() + result.meta?.get("reason")?.jsonPrimitive?.content shouldBe "User skipped" + } + + @Test + fun `should require content only for accept action`() { + shouldThrow { + ElicitResult( + action = ElicitResult.Action.Cancel, + content = buildJsonObject { put("value", "ignored") }, + ) + } + } + + // ── URLElicitationRequired error (-32042) ─────────────────────────── + + @Test + fun `UrlElicitationRequiredException carries code and serialized elicitations`() { + val elicitation = ElicitRequestURLParams( + message = "Authorize access to continue", + elicitationId = "550e8400-e29b-41d4-a716-446655440000", + url = "https://oauth.example.com/authorize", + ) + val exception = UrlElicitationRequiredException(listOf(elicitation)) + + exception.code shouldBe RPCError.ErrorCode.URL_ELICITATION_REQUIRED + exception.elicitations shouldBe listOf(elicitation) + + val data = exception.data.shouldNotBeNull() + McpJson.encodeToString(data) shouldEqualJson """ + { + "elicitations": [ + { + "message": "Authorize access to continue", + "elicitationId": "550e8400-e29b-41d4-a716-446655440000", + "url": "https://oauth.example.com/authorize", + "mode": "url" + } + ] + } + """.trimIndent() + } + + @Test + fun `fromError reconstructs typed exception for -32042 with valid data`() { + val data = McpJson.encodeToJsonElement( + UrlElicitationRequiredData( + listOf(ElicitRequestURLParams(message = "m", elicitationId = "id-1", url = "https://example.com/a")), + ), + ) + + val exception = McpException.fromError( + code = RPCError.ErrorCode.URL_ELICITATION_REQUIRED, + message = "needs auth", + data = data, + ) + + val typed = exception.shouldBeInstanceOf() + typed.message shouldBe "needs auth" + typed.elicitations.single().elicitationId shouldBe "id-1" + } + + @Test + fun `fromError returns plain McpException for unrelated code`() { + val exception = McpException.fromError( + code = RPCError.ErrorCode.INVALID_PARAMS, + message = "bad params", + data = null, + ) + (exception is UrlElicitationRequiredException) shouldBe false + exception.code shouldBe RPCError.ErrorCode.INVALID_PARAMS + } + + @Test + fun `fromError degrades to plain McpException for -32042 with malformed data`() { + val malformed = buildJsonObject { put("unexpected", "shape") } + + val exception = McpException.fromError( + code = RPCError.ErrorCode.URL_ELICITATION_REQUIRED, + message = "x", + data = malformed, + ) + + (exception is UrlElicitationRequiredException) shouldBe false + exception.code shouldBe RPCError.ErrorCode.URL_ELICITATION_REQUIRED + } + + @Test + fun `fromError degrades to plain McpException when elicitations empty`() { + val data = McpJson.encodeToJsonElement(UrlElicitationRequiredData(emptyList())) + + val exception = McpException.fromError(RPCError.ErrorCode.URL_ELICITATION_REQUIRED, "x", data) + + (exception is UrlElicitationRequiredException) shouldBe false + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/InitializeTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/InitializeTest.kt new file mode 100644 index 0000000..e5797b9 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/InitializeTest.kt @@ -0,0 +1,252 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class InitializeTest { + + @Test + fun `should serialize InitializeRequest with capabilities and meta`() { + val request = InitializeRequest( + InitializeRequestParams( + protocolVersion = "2024-11-05", + capabilities = ClientCapabilities( + sampling = ClientCapabilities.Sampling(), + roots = ClientCapabilities.Roots(listChanged = true), + elicitation = ClientCapabilities.elicitation, + experimental = buildJsonObject { + put( + "workspace-sync", + buildJsonObject { put("enabled", true) }, + ) + }, + ), + clientInfo = Implementation( + name = "dev-client", + version = "1.2.3", + title = "Dev Client", + icons = listOf(Icon(src = "https://example.com/icon.png")), + ), + meta = RequestMeta( + buildJsonObject { put("progressToken", "init-42") }, + ), + ), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": { + "sampling": {}, + "roots": { + "listChanged": true + }, + "elicitation": {}, + "experimental": { + "workspace-sync": { + "enabled": true + } + } + }, + "clientInfo": { + "name": "dev-client", + "version": "1.2.3", + "title": "Dev Client", + "icons": [ + {"src": "https://example.com/icon.png"} + ] + }, + "_meta": { + "progressToken": "init-42" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize InitializeRequest from JSON`() { + val json = """ + { + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": { + "sampling": {}, + "roots": { + "listChanged": false + }, + "experimental": { + "custom-cap": { + "enabled": true + } + } + }, + "clientInfo": { + "name": "sdk-client", + "version": "2.1.0", + "title": "SDK Client" + }, + "_meta": { + "progressToken": 99 + } + } + } + """.trimIndent() + + val request = verifyDeserialization(McpJson, json) + assertEquals(Method.Defined.Initialize, request.method) + + val params = request.params + assertEquals("2025-03-26", params.protocolVersion) + assertEquals("sdk-client", params.clientInfo.name) + assertEquals("2.1.0", params.clientInfo.version) + assertEquals("SDK Client", params.clientInfo.title) + assertEquals(ProgressToken(99), params.meta?.progressToken) + + val capabilities = params.capabilities + assertNotNull(capabilities.sampling) + assertEquals(false, capabilities.roots?.listChanged) + val experimental = capabilities.experimental + assertNotNull(experimental) + val custom = experimental["custom-cap"]?.jsonObject + assertNotNull(custom) + assertEquals(true, custom["enabled"]?.jsonPrimitive?.boolean) + } + + @Test + fun `should serialize InitializeResult with instructions`() { + val result = InitializeResult( + protocolVersion = "2024-11-05", + capabilities = ServerCapabilities( + tools = ServerCapabilities.Tools(listChanged = true), + resources = ServerCapabilities.Resources( + listChanged = true, + subscribe = true, + ), + prompts = ServerCapabilities.Prompts(listChanged = false), + logging = ServerCapabilities.Logging, + completions = buildJsonObject { put("defaultCount", 5) }, + ), + serverInfo = Implementation( + name = "demo-server", + version = "5.0.0", + websiteUrl = "https://example.com/server", + ), + instructions = "Call the `read` tool to fetch files.", + meta = buildJsonObject { put("issuedAt", "2025-01-12T15:00:58Z") }, + ) + + verifySerialization( + result, + McpJson, + """ + { + "protocolVersion": "2024-11-05", + "capabilities": { + "tools": { + "listChanged": true + }, + "resources": { + "listChanged": true, + "subscribe": true + }, + "prompts": { + "listChanged": false + }, + "logging": {}, + "completions": { + "defaultCount": 5 + } + }, + "serverInfo": { + "name": "demo-server", + "version": "5.0.0", + "websiteUrl": "https://example.com/server" + }, + "instructions": "Call the `read` tool to fetch files.", + "_meta": { + "issuedAt": "2025-01-12T15:00:58Z" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize InitializeResult without meta - meta must be omitted`() { + val result = InitializeResult( + protocolVersion = "2025-03-26", + capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = false)), + serverInfo = Implementation(name = "test-server", version = "1.0.0"), + ) + + verifySerialization( + result, + McpJson, + """ + { + "protocolVersion": "2025-03-26", + "capabilities": { + "tools": { + "listChanged": false + } + }, + "serverInfo": { + "name": "test-server", + "version": "1.0.0" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize InitializeResult from JSON`() { + val json = """ + { + "protocolVersion": "2025-03-26", + "capabilities": { + "tools": {}, + "resources": { + "listChanged": true + } + }, + "serverInfo": { + "name": "result-server", + "version": "4.0.0" + } + } + """.trimIndent() + + val result = verifyDeserialization(McpJson, json) + + assertEquals("2025-03-26", result.protocolVersion) + assertEquals("result-server", result.serverInfo.name) + assertEquals("4.0.0", result.serverInfo.version) + assertNull(result.instructions) + assertNull(result.meta) + + val capabilities = result.capabilities + assertNotNull(capabilities.tools) + assertEquals(true, capabilities.resources?.listChanged) + assertNull(capabilities.resources?.subscribe) + assertNull(capabilities.prompts) + assertNull(capabilities.logging) + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/JsonRpcTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/JsonRpcTest.kt new file mode 100644 index 0000000..7de360d --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/JsonRpcTest.kt @@ -0,0 +1,479 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.kotest.assertions.json.shouldEqualJson +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeSameInstanceAs +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull + +class JsonRpcTest { + + @Test + fun `should convert Request to JSONRPCRequest`() { + val request = ListToolsRequest( + PaginatedRequestParams( + cursor = "page-2", + meta = RequestMeta( + buildJsonObject { + put("progressToken", "token-123") + }, + ), + ), + ) + + val jsonRpc = request.toJSON() + + assertEquals(Method.Defined.ToolsList.value, jsonRpc.method) + val params = jsonRpc.params?.jsonObject + assertNotNull(params) + assertEquals("page-2", params["cursor"]?.jsonPrimitive?.content) + val meta = params["_meta"]?.jsonObject + assertNotNull(meta) + assertEquals("token-123", meta["progressToken"]?.jsonPrimitive?.content) + } + + @Test + fun `should convert JSONRPCRequest to Request`() { + val jsonRpc = verifyDeserialization( + McpJson, + """ + { + "id": 17, + "method": "tools/list", + "params": { + "cursor": "page-5", + "_meta": { + "progressToken": 42 + } + }, + "jsonrpc": "2.0" + } + """.trimIndent(), + ) + + val request = jsonRpc.fromJSON() + val listToolsRequest = assertIs(request) + val decodedParams = assertNotNull(listToolsRequest.params) + assertEquals("page-5", decodedParams.cursor) + val meta = decodedParams.meta?.json + assertNotNull(meta) + assertEquals(42, meta["progressToken"]?.jsonPrimitive?.int) + } + + @Test + fun `should convert Notification to JSONRPCNotification`() { + val notification = LoggingMessageNotification( + LoggingMessageNotificationParams( + level = LoggingLevel.Warning, + data = buildJsonObject { put("message", "Disk space low") }, + logger = "disk-monitor", + meta = buildJsonObject { put("requestId", "req-99") }, + ), + ) + + val json = McpJson.encodeToString(notification.toJSON()) + + json shouldEqualJson """ + { + "method": "notifications/message", + "params": { + "level": "warning", + "data": { + "message": "Disk space low" + }, + "logger": "disk-monitor", + "_meta": { + "requestId": "req-99" + } + }, + "jsonrpc": "2.0" + } + """.trimIndent() + } + + @Test + fun `should convert JSONRPCNotification to Notification`() { + val jsonRpc = verifyDeserialization( + McpJson, + """ + { + "method": "notifications/message", + "params": { + "level": "error", + "data": { + "lines": { + "count": 3 + } + }, + "logger": "pipeline", + "_meta": { + "traceIds": ["abc"] + } + }, + "jsonrpc": "2.0" + } + """.trimIndent(), + ) + + val notification = jsonRpc.fromJSON() + val messageNotification = assertIs(notification) + val decodedParams = messageNotification.params + assertEquals(LoggingLevel.Error, decodedParams.level) + val data = decodedParams.data.jsonObject + assertEquals(3, data["lines"]?.jsonObject?.get("count")?.jsonPrimitive?.int) + assertEquals("pipeline", decodedParams.logger) + val meta = decodedParams.meta + assertNotNull(meta) + val traceIds = meta["traceIds"]?.jsonArray + assertNotNull(traceIds) + assertEquals("abc", traceIds.first().jsonPrimitive.content) + } + + @Test + fun `should serialize JSONRPCRequest with params`() { + val request = JSONRPCRequest( + id = RequestId("req-1"), + method = "tools/list", + params = buildJsonObject { + put("cursor", "abc") + put("includeInactive", true) + }, + ) + + verifySerialization( + request, + McpJson, + """ + { + "id": "req-1", + "method": "tools/list", + "params": { + "cursor": "abc", + "includeInactive": true + }, + "jsonrpc": "2.0" + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize JSONRPCRequest with numeric id`() { + val json = """ + { + "id": 42, + "method": "resources/read", + "params": { + "uri": "file:///tmp/readme.md" + }, + "jsonrpc": "2.0" + } + """.trimIndent() + + val request = verifyDeserialization(McpJson, json) + + val id = request.id + assertIs(id) + assertEquals(42L, id.value) + assertEquals("resources/read", request.method) + val params = request.params?.jsonObject + assertNotNull(params) + assertEquals("file:///tmp/readme.md", params["uri"]?.jsonPrimitive?.content) + } + + @Test + fun `should serialize JSONRPCNotification with params`() { + val notification = JSONRPCNotification( + method = "notifications/log", + params = buildJsonObject { + put("level", "info") + put("message", "Completed operation") + }, + ) + + verifySerialization( + notification, + McpJson, + """ + { + "method": "notifications/log", + "params": { + "level": "info", + "message": "Completed operation" + }, + "jsonrpc": "2.0" + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize JSONRPCNotification`() { + val json = """ + { + "method": "notifications/progress", + "params": { + "progress": 50, + "total": 100 + }, + "jsonrpc": "2.0" + } + """.trimIndent() + + val notification = verifyDeserialization(McpJson, json) + assertEquals("notifications/progress", notification.method) + val params = notification.params?.jsonObject + assertNotNull(params) + assertEquals(50, params["progress"]?.jsonPrimitive?.int) + assertEquals(100, params["total"]?.jsonPrimitive?.int) + } + + @Test + fun `should serialize JSONRPCResponse with result`() { + val response = JSONRPCResponse( + id = RequestId("call-1"), + result = EmptyResult( + meta = buildJsonObject { put("durationMs", 15) }, + ), + ) + + verifySerialization( + response, + McpJson, + """ + { + "id": "call-1", + "result": { + "_meta": { + "durationMs": 15 + } + }, + "jsonrpc": "2.0" + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize JSONRPCResponse with EmptyResult - null meta must be omitted`() { + val response = JSONRPCResponse( + id = RequestId("call-2"), + result = EmptyResult(), // meta = null by default + ) + + verifySerialization( + response, + McpJson, + """ + { + "id": "call-2", + "result": {}, + "jsonrpc": "2.0" + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize JSONRPCResponse with EmptyResult`() { + val json = """ + { + "id": 7, + "jsonrpc": "2.0", + "result": { + "_meta": { + "cached": true + } + } + } + """.trimIndent() + + val response = verifyDeserialization(McpJson, json) + val result = response.result + assertIs(result) + assertEquals(RequestId.NumberId(7L), response.id) + val meta = result.meta + assertNotNull(meta) + assertEquals(true, meta["cached"]?.jsonPrimitive?.boolean) + } + + @Test + fun `should serialize JSONRPCError`() { + val error = JSONRPCError( + id = RequestId(99), + error = RPCError( + code = RPCError.ErrorCode.METHOD_NOT_FOUND, + message = "Method not found", + data = buildJsonObject { put("method", "tools/unknown") }, + ), + ) + + verifySerialization( + error, + McpJson, + """ + { + "id": 99, + "error": { + "code": -32601, + "message": "Method not found", + "data": { + "method": "tools/unknown" + } + }, + "jsonrpc": "2.0" + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize JSONRPCError`() { + val json = """ + { + "id": "req-404", + "jsonrpc": "2.0", + "error": { + "code": -32602, + "message": "Invalid params", + "data": { + "field": "limit", + "reason": "must be positive" + } + } + } + """.trimIndent() + + val error = verifyDeserialization(McpJson, json) + assertEquals(RequestId("req-404"), error.id) + assertEquals(RPCError.ErrorCode.INVALID_PARAMS, error.error.code) + assertEquals("Invalid params", error.error.message) + + val data = error.error.data?.jsonObject + assertNotNull(data) + assertEquals("limit", data["field"]?.jsonPrimitive?.content) + assertEquals("must be positive", data["reason"]?.jsonPrimitive?.content) + } + + @Test + fun `should decode JSONRPCMessage as request`() { + val json = """ + { + "id": "msg-1", + "method": "sampling/create", + "params": { + "model": "gpt", + "prompt": "Hello" + }, + "jsonrpc": "2.0" + } + """.trimIndent() + + val message = verifyDeserialization(McpJson, json) + val request = assertIs(message) + assertEquals("sampling/create", request.method) + val params = request.params?.jsonObject + assertNotNull(params) + assertEquals("gpt", params["model"]?.jsonPrimitive?.content) + assertEquals("Hello", params["prompt"]?.jsonPrimitive?.content) + } + + @Test + fun `should decode JSONRPCMessage as error response`() { + val json = """ + { + "id": 123, + "jsonrpc": "2.0", + "error": { + "code": -32001, + "message": "Request timeout", + "data": { + "timeoutMs": 1000 + } + } + } + """.trimIndent() + + val message = verifyDeserialization(McpJson, json) + val error = assertIs(message) + assertEquals(RPCError.ErrorCode.REQUEST_TIMEOUT, error.error.code) + val data = error.error.data?.jsonObject + assertNotNull(data) + assertEquals(1000, data["timeoutMs"]?.jsonPrimitive?.int) + } + + @Test + fun `should encode JSONRPCMessage polymorphically`() { + val message: JSONRPCMessage = JSONRPCNotification( + method = "notifications/log", + params = buildJsonObject { put("message", "Polymorphic") }, + ) + + verifySerialization( + message, + McpJson, + """ + { + "method": "notifications/log", + "params": { + "message": "Polymorphic" + }, + "jsonrpc": "2.0" + } + """.trimIndent(), + ) + } + + @Test + fun `should create JSONRPCRequest with string ID`() { + val params = buildJsonObject { + put("foo", "bar") + } + val request = JSONRPCRequest( + id = "req-42", + method = "notifications/log", + params = params, + ) + request.id shouldBe RequestId("req-42") + request.method shouldBe "notifications/log" + request.params shouldBeSameInstanceAs params + } + + @Test + fun `should create JSONRPCRequest with numeric ID`() { + val params = buildJsonObject { + put("foo", "bar") + } + val request = JSONRPCRequest( + id = 42, + method = "notifications/log", + params = params, + ) + request.id shouldBe RequestId(42) + request.method shouldBe "notifications/log" + request.params shouldBeSameInstanceAs params + } + + @Test + fun `should deserialize JSONRPCEmptyMessage`() { + val json = """ + { + "jsonrpc": "2.0" + } + """.trimIndent() + + val message = McpJson.decodeFromString(json) + message shouldBeSameInstanceAs JSONRPCEmptyMessage + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/LoggingTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/LoggingTest.kt new file mode 100644 index 0000000..bc03a47 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/LoggingTest.kt @@ -0,0 +1,171 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class LoggingTest { + + @Test + fun `should serialize LoggingLevel to expected schema values`() { + assertEquals("\"debug\"", McpJson.encodeToString(LoggingLevel.Debug)) + assertEquals("\"info\"", McpJson.encodeToString(LoggingLevel.Info)) + assertEquals("\"notice\"", McpJson.encodeToString(LoggingLevel.Notice)) + assertEquals("\"warning\"", McpJson.encodeToString(LoggingLevel.Warning)) + assertEquals("\"error\"", McpJson.encodeToString(LoggingLevel.Error)) + assertEquals("\"critical\"", McpJson.encodeToString(LoggingLevel.Critical)) + assertEquals("\"alert\"", McpJson.encodeToString(LoggingLevel.Alert)) + assertEquals("\"emergency\"", McpJson.encodeToString(LoggingLevel.Emergency)) + } + + @Test + fun `should deserialize LoggingLevel from schema values`() { + assertEquals(LoggingLevel.Debug, McpJson.decodeFromString("\"debug\"")) + assertEquals(LoggingLevel.Info, McpJson.decodeFromString("\"info\"")) + assertEquals(LoggingLevel.Notice, McpJson.decodeFromString("\"notice\"")) + assertEquals(LoggingLevel.Warning, McpJson.decodeFromString("\"warning\"")) + assertEquals(LoggingLevel.Error, McpJson.decodeFromString("\"error\"")) + assertEquals(LoggingLevel.Critical, McpJson.decodeFromString("\"critical\"")) + assertEquals(LoggingLevel.Alert, McpJson.decodeFromString("\"alert\"")) + assertEquals(LoggingLevel.Emergency, McpJson.decodeFromString("\"emergency\"")) + } + + @Test + fun `should serialize SetLevelRequest with meta`() { + val request = SetLevelRequest( + SetLevelRequestParams( + level = LoggingLevel.Warning, + meta = RequestMeta( + buildJsonObject { put("progressToken", "log-42") }, + ), + ), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "logging/setLevel", + "params": { + "level": "warning", + "_meta": { + "progressToken": "log-42" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize SetLevelRequest with numeric progress token`() { + val json = """ + { + "method": "logging/setLevel", + "params": { + "level": "alert", + "_meta": { + "progressToken": 1001 + } + } + } + """.trimIndent() + + val request = verifyDeserialization(McpJson, json) + assertEquals(Method.Defined.LoggingSetLevel, request.method) + assertEquals(LoggingLevel.Alert, request.params.level) + assertEquals(ProgressToken(1001), request.params.meta?.progressToken) + } + + @Test + fun `should serialize LoggingMessageNotification with logger`() { + val notification = LoggingMessageNotification( + LoggingMessageNotificationParams( + level = LoggingLevel.Error, + data = buildJsonObject { put("message", "Disk space critically low") }, + logger = "infra.monitor", + meta = buildJsonObject { put("source", "node-1") }, + ), + ) + + verifySerialization( + notification, + McpJson, + """ + { + "method": "notifications/message", + "params": { + "level": "error", + "data": { + "message": "Disk space critically low" + }, + "logger": "infra.monitor", + "_meta": { + "source": "node-1" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize LoggingMessageNotification with text data`() { + val json = """ + { + "method": "notifications/message", + "params": { + "level": "info", + "data": "Service started successfully" + } + } + """.trimIndent() + + val notification = verifyDeserialization(McpJson, json) + assertEquals(Method.Defined.NotificationsMessage, notification.method) + + val params = notification.params + assertEquals(LoggingLevel.Info, params.level) + assertEquals("Service started successfully", params.data.jsonPrimitive.content) + assertNull(params.logger) + assertNull(params.meta) + } + + @Test + fun `should deserialize LoggingMessageNotification with structured data`() { + val json = """ + { + "method": "notifications/message", + "params": { + "level": "critical", + "data": { + "code": "DB_CONN_FAIL", + "retry": false + }, + "logger": "database.monitor", + "_meta": { + "requestId": "req-77" + } + } + } + """.trimIndent() + + val notification = verifyDeserialization(McpJson, json) + val params = notification.params + assertEquals(LoggingLevel.Critical, params.level) + val data = params.data.jsonObject + assertEquals("DB_CONN_FAIL", data["code"]?.jsonPrimitive?.content) + assertEquals(false, data["retry"]?.jsonPrimitive?.boolean) + assertEquals("database.monitor", params.logger) + val meta = params.meta + assertEquals("req-77", meta?.get("requestId")?.jsonPrimitive?.content) + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/NotificationTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/NotificationTest.kt new file mode 100644 index 0000000..500b303 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/NotificationTest.kt @@ -0,0 +1,470 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class NotificationTest { + + @Test + fun `should serialize CancelledNotification with reason and meta`() { + val notification = CancelledNotification( + CancelledNotificationParams( + requestId = RequestId("req-1"), + reason = "User requested cancellation", + meta = buildJsonObject { put("source", "client") }, + ), + ) + + verifySerialization( + notification, + McpJson, + """ + { + "method": "notifications/cancelled", + "params": { + "requestId": "req-1", + "reason": "User requested cancellation", + "_meta": { + "source": "client" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize CancelledNotification with numeric request id`() { + val json = """ + { + "method": "notifications/cancelled", + "params": { + "requestId": 42, + "reason": "Timeout reached" + } + } + """.trimIndent() + + val notification = verifyDeserialization(McpJson, json) + val params = notification.params + + assertEquals(Method.Defined.NotificationsCancelled, notification.method) + assertEquals(RequestId(42), params.requestId) + assertEquals("Timeout reached", params.reason) + assertNull(params.meta) + } + + @Test + fun `should serialize InitializedNotification with meta`() { + val notification = InitializedNotification( + BaseNotificationParams( + meta = buildJsonObject { put("readyAt", "2025-01-12T15:00:58Z") }, + ), + ) + + verifySerialization( + notification, + McpJson, + """ + { + "method": "notifications/initialized", + "params": { + "_meta": { + "readyAt": "2025-01-12T15:00:58Z" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize InitializedNotification without params`() { + val json = """ + { + "method": "notifications/initialized" + } + """.trimIndent() + + val notification = verifyDeserialization(McpJson, json) + + assertEquals(Method.Defined.NotificationsInitialized, notification.method) + assertNull(notification.params) + } + + @Test + fun `should serialize ProgressNotification with all fields`() { + val notification = ProgressNotification( + ProgressNotificationParams( + progressToken = ProgressToken("task-42"), + progress = 0.6, + total = 1.0, + message = "Syncing repository", + meta = buildJsonObject { put("stage", "download") }, + ), + ) + + verifySerialization( + notification, + McpJson, + """ + { + "method": "notifications/progress", + "params": { + "progressToken": "task-42", + "progress": 0.6, + "total": 1.0, + "message": "Syncing repository", + "_meta": { + "stage": "download" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize ProgressNotification with numeric token`() { + val json = """ + { + "method": "notifications/progress", + "params": { + "progressToken": 7, + "progress": 0.25 + } + } + """.trimIndent() + + val notification = verifyDeserialization(McpJson, json) + val params = notification.params + + assertEquals(Method.Defined.NotificationsProgress, notification.method) + assertEquals(ProgressToken(7), params.progressToken) + assertEquals(0.25, params.progress) + assertNull(params.total) + assertNull(params.message) + assertNull(params.meta) + } + + @Test + fun `should serialize ResourceUpdatedNotification with meta`() { + val notification = ResourceUpdatedNotification( + ResourceUpdatedNotificationParams( + uri = "file:///workspace/README.md", + meta = buildJsonObject { put("checksum", "abcd1234") }, + ), + ) + + verifySerialization( + notification, + McpJson, + """ + { + "method": "notifications/resources/updated", + "params": { + "uri": "file:///workspace/README.md", + "_meta": { + "checksum": "abcd1234" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize ResourceUpdatedNotification`() { + val json = """ + { + "method": "notifications/resources/updated", + "params": { + "uri": "file:///docs/guide.md", + "_meta": { + "etag": "W/\"42\"" + } + } + } + """.trimIndent() + + val notification = verifyDeserialization(McpJson, json) + val params = notification.params + + assertEquals(Method.Defined.NotificationsResourcesUpdated, notification.method) + assertEquals("file:///docs/guide.md", params.uri) + assertEquals("W/\"42\"", params.meta?.get("etag")?.jsonPrimitive?.content) + } + + @Test + fun `should serialize PromptListChangedNotification with meta`() { + val notification = PromptListChangedNotification( + BaseNotificationParams( + meta = buildJsonObject { put("reason", "catalog-updated") }, + ), + ) + + verifySerialization( + notification, + McpJson, + """ + { + "method": "notifications/prompts/list_changed", + "params": { + "_meta": { + "reason": "catalog-updated" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize PromptListChangedNotification without params`() { + val json = """ + { + "method": "notifications/prompts/list_changed" + } + """.trimIndent() + + val notification = verifyDeserialization(McpJson, json) + + assertEquals(Method.Defined.NotificationsPromptsListChanged, notification.method) + assertNull(notification.params) + } + + @Test + fun `should serialize ResourceListChangedNotification with meta`() { + val notification = ResourceListChangedNotification( + BaseNotificationParams( + meta = buildJsonObject { put("reason", "subscription-updated") }, + ), + ) + + verifySerialization( + notification, + McpJson, + """ + { + "method": "notifications/resources/list_changed", + "params": { + "_meta": { + "reason": "subscription-updated" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize RootsListChangedNotification with meta`() { + val notification = RootsListChangedNotification( + BaseNotificationParams( + meta = buildJsonObject { put("reason", "workspace-moved") }, + ), + ) + + verifySerialization( + notification, + McpJson, + """ + { + "method": "notifications/roots/list_changed", + "params": { + "_meta": { + "reason": "workspace-moved" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ToolListChangedNotification with meta`() { + val notification = ToolListChangedNotification( + BaseNotificationParams( + meta = buildJsonObject { put("reason", "tool-added") }, + ), + ) + + verifySerialization( + notification, + McpJson, + """ + { + "method": "notifications/tools/list_changed", + "params": { + "_meta": { + "reason": "tool-added" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ElicitationCompleteNotification with meta`() { + val notification = ElicitationCompleteNotification( + ElicitationCompleteNotificationParams( + elicitationId = "elicit-42", + meta = buildJsonObject { put("source", "oauth-flow") }, + ), + ) + + verifySerialization( + notification, + McpJson, + """ + { + "method": "notifications/elicitation/complete", + "params": { + "elicitationId": "elicit-42", + "_meta": { + "source": "oauth-flow" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize ElicitationCompleteNotification`() { + val json = """ + { + "method": "notifications/elicitation/complete", + "params": { + "elicitationId": "elicit-99" + } + } + """.trimIndent() + + val notification = verifyDeserialization(McpJson, json) + val params = notification.params + + assertEquals(Method.Defined.NotificationsElicitationComplete, notification.method) + assertEquals("elicit-99", params.elicitationId) + assertNull(params.meta) + } + + @Test + fun `should serialize TaskStatusNotification with all fields`() { + val notification = TaskStatusNotification( + TaskStatusNotificationParams( + taskId = "task-1", + status = TaskStatus.Working, + statusMessage = "Processing data", + createdAt = "2025-01-01T00:00:00Z", + lastUpdatedAt = "2025-01-01T00:01:00Z", + ttl = 60000, + pollInterval = 5000, + meta = buildJsonObject { put("source", "worker-1") }, + ), + ) + + verifySerialization( + notification, + McpJson, + """ + { + "method": "notifications/tasks/status", + "params": { + "taskId": "task-1", + "status": "working", + "statusMessage": "Processing data", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:01:00Z", + "ttl": 60000, + "pollInterval": 5000, + "_meta": { + "source": "worker-1" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize TaskStatusNotification with minimal fields`() { + val notification = TaskStatusNotification( + TaskStatusNotificationParams( + taskId = "task-2", + status = TaskStatus.Completed, + createdAt = "2025-01-01T00:00:00Z", + lastUpdatedAt = "2025-01-01T00:02:00Z", + ttl = null, + ), + ) + + verifySerialization( + notification, + McpJson, + """ + { + "method": "notifications/tasks/status", + "params": { + "taskId": "task-2", + "status": "completed", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:02:00Z" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize TaskStatusNotification`() { + val json = """ + { + "method": "notifications/tasks/status", + "params": { + "taskId": "task-3", + "status": "failed", + "statusMessage": "Connection lost", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:03:00Z", + "ttl": 30000, + "pollInterval": 1000 + } + } + """.trimIndent() + + val notification = verifyDeserialization(McpJson, json) + val params = notification.params!! + + assertEquals(Method.Defined.NotificationsTasksStatus, notification.method) + assertEquals("task-3", params.taskId) + assertEquals(TaskStatus.Failed, params.status) + assertEquals("Connection lost", params.statusMessage) + assertEquals("2025-01-01T00:00:00Z", params.createdAt) + assertEquals("2025-01-01T00:03:00Z", params.lastUpdatedAt) + assertEquals(30000L, params.ttl) + assertEquals(1000L, params.pollInterval) + assertNull(params.meta) + } + + @Test + fun `should deserialize TaskStatusNotification without params`() { + val json = """ + { + "method": "notifications/tasks/status" + } + """.trimIndent() + + val notification = verifyDeserialization(McpJson, json) + + assertEquals(Method.Defined.NotificationsTasksStatus, notification.method) + assertNull(notification.params) + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PingRequestTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PingRequestTest.kt new file mode 100644 index 0000000..72bddb0 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PingRequestTest.kt @@ -0,0 +1,100 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull + +class PingRequestTest { + + @Test + fun `should serialize PingRequest without params`() { + val request = PingRequest() + + verifySerialization( + request, + McpJson, + """ + { + "method": "ping" + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize PingRequest with meta`() { + val request = PingRequest( + BaseRequestParams( + meta = RequestMeta( + buildJsonObject { put("progressToken", "ping-42") }, + ), + ), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "ping", + "params": { + "_meta": { + "progressToken": "ping-42" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should convert PingRequest to JSONRPCRequest`() { + val request = PingRequest( + BaseRequestParams( + meta = RequestMeta( + buildJsonObject { put("progressToken", 99) }, + ), + ), + ) + + val jsonRpc = request.toJSON() + + assertEquals("ping", jsonRpc.method) + val params = assertNotNull(jsonRpc.params).jsonObject + val meta = params["_meta"]?.jsonObject + assertNotNull(meta) + assertEquals(99, meta["progressToken"]?.jsonPrimitive?.int) + } + + @Test + fun `should deserialize JSONRPCRequest to PingRequest`() { + val json = """ + { + "id": "ping-1", + "method": "ping", + "jsonrpc": "2.0", + "params": { + "_meta": { + "progressToken": "pong-1" + } + } + } + """.trimIndent() + + val jsonRpc = verifyDeserialization(McpJson, json) + val request = jsonRpc.fromJSON() + + val pingRequest = assertIs(request) + val meta = pingRequest.params?.meta?.json + assertNotNull(meta) + assertEquals("pong-1", meta["progressToken"]?.jsonPrimitive?.content) + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaTest.kt new file mode 100644 index 0000000..904454f --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaTest.kt @@ -0,0 +1,479 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.types.shouldBeInstanceOf +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization +import kotlinx.serialization.SerializationException +import kotlin.test.Test + +class PrimitiveSchemaTest { + + // ── StringSchema ──────────────────────────────────────────────────── + + @Test + fun `should serialize StringSchema with minimal fields`() { + verifySerialization( + StringSchema() as PrimitiveSchemaDefinition, + McpJson, + """{"type": "string"}""", + ) + } + + @Test + fun `should serialize StringSchema with all fields`() { + val schema = StringSchema( + title = "Email address", + description = "User email", + minLength = 5, + maxLength = 100, + format = StringSchemaFormat.Email, + default = "user@example.com", + ) + + verifySerialization( + schema as PrimitiveSchemaDefinition, + McpJson, + """ + { + "title": "Email address", + "description": "User email", + "minLength": 5, + "maxLength": 100, + "format": "email", + "default": "user@example.com", + "type": "string" + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize StringSchema from JSON`() { + val json = """ + { + "type": "string", + "title": "Name", + "format": "uri" + } + """.trimIndent() + + val schema = verifyDeserialization(McpJson, json) + schema.shouldBeInstanceOf() + schema.title shouldBe "Name" + schema.format shouldBe StringSchemaFormat.Uri + } + + // ── StringSchemaFormat ────────────────────────────────────────────── + + @Test + fun `should serialize all StringSchemaFormat values`() { + val cases = mapOf( + StringSchemaFormat.Email to "email", + StringSchemaFormat.Uri to "uri", + StringSchemaFormat.Date to "date", + StringSchemaFormat.DateTime to "date-time", + ) + for ((format, expectedValue) in cases) { + val schema = StringSchema(format = format) + val json = McpJson.encodeToString(schema) + json shouldContain "\"format\":\"$expectedValue\"" + } + } + + // ── IntegerSchema ─────────────────────────────────────────────────── + + @Test + fun `should serialize IntegerSchema with minimal fields`() { + verifySerialization( + IntegerSchema() as PrimitiveSchemaDefinition, + McpJson, + """{"type": "integer"}""", + ) + } + + @Test + fun `should serialize IntegerSchema with all fields`() { + verifySerialization( + IntegerSchema( + title = "Age", + description = "User age", + minimum = 0, + maximum = 150, + default = 25, + ) as PrimitiveSchemaDefinition, + McpJson, + """ + { + "title": "Age", + "description": "User age", + "minimum": 0, + "maximum": 150, + "default": 25, + "type": "integer" + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize IntegerSchema from JSON`() { + val json = """{"type": "integer", "minimum": 1, "maximum": 10}""" + val schema = verifyDeserialization(McpJson, json) + schema.shouldBeInstanceOf() + schema.minimum shouldBe 1 + schema.maximum shouldBe 10 + } + + // ── DoubleSchema ──────────────────────────────────────────────────── + + @Test + fun `should serialize DoubleSchema with minimal fields`() { + verifySerialization( + DoubleSchema() as PrimitiveSchemaDefinition, + McpJson, + """{"type": "number"}""", + ) + } + + @Test + fun `should serialize DoubleSchema with all fields`() { + verifySerialization( + DoubleSchema( + title = "Score", + description = "Test score", + minimum = 0.0, + maximum = 100.0, + default = 50.0, + ) as PrimitiveSchemaDefinition, + McpJson, + """ + { + "title": "Score", + "description": "Test score", + "minimum": 0.0, + "maximum": 100.0, + "default": 50.0, + "type": "number" + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize DoubleSchema from JSON`() { + val json = """{"type": "number", "minimum": 0.5}""" + val schema = verifyDeserialization(McpJson, json) + schema.shouldBeInstanceOf() + schema.minimum shouldBe 0.5 + } + + // ── BooleanSchema ─────────────────────────────────────────────────── + + @Test + fun `should serialize BooleanSchema with minimal fields`() { + verifySerialization( + BooleanSchema() as PrimitiveSchemaDefinition, + McpJson, + """{"type": "boolean"}""", + ) + } + + @Test + fun `should serialize BooleanSchema with all fields`() { + verifySerialization( + BooleanSchema( + title = "Agree", + description = "Terms acceptance", + default = false, + ) as PrimitiveSchemaDefinition, + McpJson, + """ + { + "title": "Agree", + "description": "Terms acceptance", + "default": false, + "type": "boolean" + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize BooleanSchema from JSON`() { + val json = """{"type": "boolean", "default": true}""" + val schema = verifyDeserialization(McpJson, json) + schema.shouldBeInstanceOf() + schema.default shouldBe true + } + + // ── UntitledSingleSelectEnumSchema ────────────────────────────────── + + @Test + fun `should serialize UntitledSingleSelectEnumSchema`() { + verifySerialization( + UntitledSingleSelectEnumSchema( + title = "Color", + enumValues = listOf("Red", "Green", "Blue"), + default = "Red", + ) as PrimitiveSchemaDefinition, + McpJson, + """ + { + "title": "Color", + "enum": ["Red", "Green", "Blue"], + "default": "Red", + "type": "string" + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize UntitledSingleSelectEnumSchema from JSON`() { + val json = """ + { + "type": "string", + "enum": ["A", "B", "C"] + } + """.trimIndent() + + val schema = verifyDeserialization(McpJson, json) + schema.shouldBeInstanceOf() + schema.enumValues shouldBe listOf("A", "B", "C") + } + + // ── TitledSingleSelectEnumSchema ──────────────────────────────────── + + @Test + fun `should serialize TitledSingleSelectEnumSchema`() { + verifySerialization( + TitledSingleSelectEnumSchema( + title = "Color", + oneOf = listOf( + EnumOption(const = "#FF0000", title = "Red"), + EnumOption(const = "#00FF00", title = "Green"), + ), + default = "#FF0000", + ) as PrimitiveSchemaDefinition, + McpJson, + """ + { + "title": "Color", + "oneOf": [ + {"const": "#FF0000", "title": "Red"}, + {"const": "#00FF00", "title": "Green"} + ], + "default": "#FF0000", + "type": "string" + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize TitledSingleSelectEnumSchema from JSON`() { + val json = """ + { + "type": "string", + "oneOf": [ + {"const": "a", "title": "Alpha"}, + {"const": "b", "title": "Beta"} + ] + } + """.trimIndent() + + val schema = verifyDeserialization(McpJson, json) + schema.shouldBeInstanceOf() + schema.oneOf shouldHaveSize 2 + schema.oneOf[0].const shouldBe "a" + schema.oneOf[0].title shouldBe "Alpha" + } + + // ── UntitledMultiSelectEnumSchema ──────────────────────────────────── + + @Test + fun `should serialize UntitledMultiSelectEnumSchema`() { + verifySerialization( + UntitledMultiSelectEnumSchema( + title = "Colors", + minItems = 1, + maxItems = 3, + items = UntitledMultiSelectEnumSchema.Items(enumValues = listOf("Red", "Green", "Blue")), + default = listOf("Red"), + ) as PrimitiveSchemaDefinition, + McpJson, + """ + { + "title": "Colors", + "minItems": 1, + "maxItems": 3, + "items": { + "enum": ["Red", "Green", "Blue"], + "type": "string" + }, + "default": ["Red"], + "type": "array" + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize UntitledMultiSelectEnumSchema from JSON`() { + val json = """ + { + "type": "array", + "items": { + "type": "string", + "enum": ["X", "Y"] + } + } + """.trimIndent() + + val schema = verifyDeserialization(McpJson, json) + schema.shouldBeInstanceOf() + schema.items.enumValues shouldBe listOf("X", "Y") + } + + // ── TitledMultiSelectEnumSchema ────────────────────────────────────── + + @Test + fun `should serialize TitledMultiSelectEnumSchema`() { + verifySerialization( + TitledMultiSelectEnumSchema( + title = "Colors", + minItems = 1, + maxItems = 2, + items = TitledMultiSelectEnumSchema.Items( + anyOf = listOf( + EnumOption(const = "#FF0000", title = "Red"), + EnumOption(const = "#00FF00", title = "Green"), + ), + ), + default = listOf("#FF0000"), + ) as PrimitiveSchemaDefinition, + McpJson, + """ + { + "title": "Colors", + "minItems": 1, + "maxItems": 2, + "items": { + "anyOf": [ + {"const": "#FF0000", "title": "Red"}, + {"const": "#00FF00", "title": "Green"} + ] + }, + "default": ["#FF0000"], + "type": "array" + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize TitledMultiSelectEnumSchema from JSON`() { + val json = """ + { + "type": "array", + "items": { + "anyOf": [ + {"const": "x", "title": "X"}, + {"const": "y", "title": "Y"} + ] + } + } + """.trimIndent() + + val schema = verifyDeserialization(McpJson, json) + schema.shouldBeInstanceOf() + schema.items.anyOf shouldHaveSize 2 + schema.items.anyOf[0].const shouldBe "x" + } + + // ── LegacyTitledEnumSchema ──────────────────────────────────────────── + + @Test + fun `should serialize LegacyTitledEnumSchema`() { + verifySerialization( + LegacyTitledEnumSchema( + title = "Status", + enumValues = listOf("opt1", "opt2", "opt3"), + enumNames = listOf("Option One", "Option Two", "Option Three"), + default = "opt1", + ) as PrimitiveSchemaDefinition, + McpJson, + """ + { + "title": "Status", + "enum": ["opt1", "opt2", "opt3"], + "enumNames": ["Option One", "Option Two", "Option Three"], + "default": "opt1", + "type": "string" + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize LegacyTitledEnumSchema from JSON`() { + val json = """ + { + "type": "string", + "enum": ["a", "b"], + "enumNames": ["Alpha", "Beta"] + } + """.trimIndent() + + val schema = verifyDeserialization(McpJson, json) + schema.shouldBeInstanceOf() + schema.enumValues shouldBe listOf("a", "b") + schema.enumNames shouldBe listOf("Alpha", "Beta") + } + + // ── Error cases ───────────────────────────────────────────────────── + + @Test + fun `should throw on unknown type`() { + val json = """{"type": "unknown"}""" + shouldThrow { + McpJson.decodeFromString(json) + } + } + + // ── RequestedSchema with typed properties ─────────────────────────── + + @Test + fun `should round-trip RequestedSchema with mixed property types`() { + val schema = ElicitRequestParams.RequestedSchema( + properties = mapOf( + "name" to StringSchema(title = "Name"), + "age" to IntegerSchema(minimum = 0), + "confirmed" to BooleanSchema(default = false), + "color" to UntitledSingleSelectEnumSchema(enumValues = listOf("Red", "Blue")), + ), + required = listOf("name"), + ) + + verifySerialization( + schema, + McpJson, + """ + { + "properties": { + "name": {"title": "Name", "type": "string"}, + "age": {"minimum": 0, "type": "integer"}, + "confirmed": {"default": false, "type": "boolean"}, + "color": {"enum": ["Red", "Blue"], "type": "string"} + }, + "required": ["name"], + "type": "object" + } + """.trimIndent(), + ) + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PromptsTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PromptsTest.kt new file mode 100644 index 0000000..d7101d3 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PromptsTest.kt @@ -0,0 +1,473 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.kotest.assertions.json.shouldEqualJson +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.double +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class PromptsTest { + + @Test + fun `should serialize PromptArgument with minimal fields`() { + val argument = PromptArgument(name = "repository") + + verifySerialization( + argument, + McpJson, + """ + { + "name": "repository" + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize PromptArgument with all fields`() { + val argument = PromptArgument( + name = "language", + description = "Programming language to use", + required = true, + title = "Language", + ) + + verifySerialization( + argument, + McpJson, + """ + { + "name": "language", + "description": "Programming language to use", + "required": true, + "title": "Language" + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize Prompt with arguments and meta`() { + val prompt = Prompt( + name = "summarize_update", + title = "Summarize Update", + description = "Summarize the latest repository changes.", + arguments = listOf( + PromptArgument( + name = "summaryLength", + description = "Approximate length of the summary", + required = false, + title = "Summary length", + ), + ), + icons = listOf( + Icon(src = "https://example.com/icon.png"), + Icon(src = "https://example.com/icon-dark.svg", theme = Icon.Theme.Dark), + ), + meta = buildJsonObject { put("category", "status-report") }, + ) + + verifySerialization( + prompt, + McpJson, + """ + { + "name": "summarize_update", + "description": "Summarize the latest repository changes.", + "arguments": [ + { + "name": "summaryLength", + "description": "Approximate length of the summary", + "required": false, + "title": "Summary length" + } + ], + "title": "Summarize Update", + "icons": [ + { + "src": "https://example.com/icon.png" + }, + { + "src": "https://example.com/icon-dark.svg", + "theme": "dark" + } + ], + "_meta": { + "category": "status-report" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize Prompt from JSON`() { + val json = """ + { + "name": "code_review", + "description": "Generate a succinct code review.", + "title": "Code Review", + "arguments": [ + { + "name": "filePath", + "description": "Path to the file to review", + "required": true + }, + { + "name": "severity", + "title": "Issue severity" + } + ], + "icons": [ + {"src": "https://example.com/review.png"} + ], + "_meta": { + "category": "quality" + } + } + """.trimIndent() + + val prompt = verifyDeserialization(McpJson, json) + + assertEquals("code_review", prompt.name) + assertEquals("Generate a succinct code review.", prompt.description) + assertEquals("Code Review", prompt.title) + val arguments = prompt.arguments + assertNotNull(arguments) + assertEquals(2, arguments.size) + assertEquals("filePath", arguments[0].name) + assertEquals(true, arguments[0].required) + assertEquals("severity", arguments[1].name) + assertEquals("Issue severity", arguments[1].title) + val meta = prompt.meta + assertNotNull(meta) + assertEquals("quality", meta["category"]?.jsonPrimitive?.content) + } + + @Test + fun `should serialize PromptMessage with text content`() { + val message = PromptMessage( + role = Role.Assistant, + content = TextContent(text = "Provide a concise summary of the changes."), + ) + + verifySerialization( + message, + McpJson, + """ + { + "role": "assistant", + "content": { + "type": "text", + "text": "Provide a concise summary of the changes." + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize PromptMessage with resource content`() { + val json = """ + { + "role": "user", + "content": { + "type": "resource", + "resource": { + "uri": "file:///workspace/README.md", + "mimeType": "text/markdown", + "text": "# Project Overview" + } + } + } + """.trimIndent() + + val message = verifyDeserialization(McpJson, json) + + assertEquals(Role.User, message.role) + val content = message.content + assertIs(content) + val resource = assertIs(content.resource) + assertEquals("file:///workspace/README.md", resource.uri) + assertEquals("# Project Overview", resource.text) + } + + @Test + fun `should serialize and deserialize PromptReference`() { + val reference = PromptReference(name = "daily-summary", title = "Daily Summary") + + val json = McpJson.encodeToString(reference) + json shouldEqualJson """ + { + "type": "ref/prompt", + "name": "daily-summary", + "title": "Daily Summary" + } + """.trimIndent() + + val decoded = verifyDeserialization(McpJson, json) + val promptReference = assertIs(decoded) + assertEquals("daily-summary", promptReference.name) + assertEquals("Daily Summary", promptReference.title) + assertEquals(ReferenceType.Prompt, promptReference.type) + } + + @Test + fun `should serialize GetPromptRequest with arguments and meta`() { + val request = GetPromptRequest( + GetPromptRequestParams( + name = "generate_release_notes", + arguments = mapOf("version" to "1.2.3"), + meta = RequestMeta( + buildJsonObject { put("progressToken", "get-prompt-1") }, + ), + ), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "prompts/get", + "params": { + "name": "generate_release_notes", + "arguments": { + "version": "1.2.3" + }, + "_meta": { + "progressToken": "get-prompt-1" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize GetPromptRequest from JSON`() { + val json = """ + { + "method": "prompts/get", + "params": { + "name": "draft-response", + "arguments": { + "tone": "confident", + "audience": "executive" + }, + "_meta": { + "progressToken": 88 + } + } + } + """.trimIndent() + + val request = verifyDeserialization(McpJson, json) + + assertEquals(Method.Defined.PromptsGet, request.method) + val params = request.params + assertEquals("draft-response", params.name) + val args = params.arguments + assertNotNull(args) + assertEquals("confident", args["tone"]) + assertEquals("executive", args["audience"]) + assertEquals(ProgressToken(88), params.meta?.progressToken) + } + + @Test + fun `should serialize GetPromptResult with messages and meta`() { + val result = GetPromptResult( + messages = listOf( + PromptMessage( + role = Role.User, + content = TextContent(text = "Use concise language suitable for executives."), + ), + PromptMessage( + role = Role.Assistant, + content = TextContent(text = "Here is the summary you requested."), + ), + ), + description = "Executive summary response template.", + meta = buildJsonObject { put("generatedAt", "2025-01-12T15:00:58Z") }, + ) + + verifySerialization( + result, + McpJson, + """ + { + "messages": [ + { + "role": "user", + "content": { + "type": "text", + "text": "Use concise language suitable for executives." + } + }, + { + "role": "assistant", + "content": { + "type": "text", + "text": "Here is the summary you requested." + } + } + ], + "description": "Executive summary response template.", + "_meta": { + "generatedAt": "2025-01-12T15:00:58Z" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize GetPromptResult from JSON`() { + val json = """ + { + "messages": [ + { + "role": "user", + "content": { + "type": "text", + "text": "Summarize today's standup." + } + } + ], + "description": "Collect information from the team." + } + """.trimIndent() + + val result = verifyDeserialization(McpJson, json) + + val messages = result.messages + assertEquals(1, messages.size) + assertEquals(Role.User, messages.first().role) + val textContent = assertIs(messages.first().content) + assertEquals("Summarize today's standup.", textContent.text) + assertEquals("Collect information from the team.", result.description) + assertNull(result.meta) + } + + @Test + fun `should serialize ListPromptsRequest with pagination params`() { + val request = ListPromptsRequest( + PaginatedRequestParams( + cursor = "cursor-123", + meta = RequestMeta( + buildJsonObject { put("progressToken", "list-prompts-1") }, + ), + ), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "prompts/list", + "params": { + "cursor": "cursor-123", + "_meta": { + "progressToken": "list-prompts-1" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ListPromptsRequest without params`() { + val request = ListPromptsRequest() + + verifySerialization( + request, + McpJson, + """ + { + "method": "prompts/list" + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ListPromptsResult with next cursor and meta`() { + val result = ListPromptsResult( + prompts = listOf( + Prompt(name = "morning-briefing"), + Prompt(name = "incident-response"), + ), + nextCursor = "cursor-2", + meta = buildJsonObject { put("page", 1) }, + ) + + verifySerialization( + result, + McpJson, + """ + { + "prompts": [ + {"name": "morning-briefing"}, + {"name": "incident-response"} + ], + "nextCursor": "cursor-2", + "_meta": { + "page": 1 + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize ListPromptsResult from JSON`() { + val json = """ + { + "prompts": [ + { + "name": "weekly-update", + "description": "Summarize weekly activities.", + "arguments": [ + { + "name": "team", + "title": "Team name" + } + ] + } + ], + "nextCursor": "cursor-next", + "_meta": { + "page": 2, + "latencyMs": 12.5 + } + } + """.trimIndent() + + val result = verifyDeserialization(McpJson, json) + + val prompts = result.prompts + assertEquals(1, prompts.size) + val prompt = prompts.first() + assertEquals("weekly-update", prompt.name) + assertEquals("Summarize weekly activities.", prompt.description) + val args = prompt.arguments + assertNotNull(args) + assertEquals("team", args.first().name) + assertEquals("Team name", args.first().title) + assertEquals("cursor-next", result.nextCursor) + val meta = result.meta + assertNotNull(meta) + assertEquals(2, meta["page"]?.jsonPrimitive?.int) + assertEquals(12.5, meta["latencyMs"]?.jsonPrimitive?.double) + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/RequestTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/RequestTest.kt new file mode 100644 index 0000000..6087b20 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/RequestTest.kt @@ -0,0 +1,293 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.kotest.assertions.json.shouldEqualJson +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.double +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertIs +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class RequestTest { + + @OptIn(ExperimentalMcpApi::class) + @Suppress("DEPRECATION") + @Test + fun `should decode typed request metadata without discarding extensions`() { + val request = McpJson.decodeFromString( + """ + { + "method": "tools/list", + "params": { + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": { + "name": "wire-client", + "version": "1.2.3" + }, + "io.modelcontextprotocol/clientCapabilities": { + "sampling": {}, + "com.example/custom-capability": {"enabled": true} + }, + "io.modelcontextprotocol/logLevel": "warning", + "com.example/traceId": "trace-123" + } + } + } + """.trimIndent(), + ) + + val listTools = assertIs(request) + val meta = assertNotNull(listTools.params?.meta) + meta.protocolVersion shouldBe "2026-07-28" + meta.clientInfo shouldBe Implementation(name = "wire-client", version = "1.2.3") + assertNotNull(meta.clientCapabilities?.sampling) + meta.logLevel shouldBe LoggingLevel.Warning + meta["com.example/traceId"]?.jsonPrimitive?.content shouldBe "trace-123" + assertNotNull(meta.json[RequestMetaKeys.CLIENT_CAPABILITIES]?.let { it as? JsonObject }) + } + + @OptIn(ExperimentalMcpApi::class) + @Test + fun `empty client capabilities should mean no optional capabilities`() { + val json = Json.parseToJsonElement( + """{"${RequestMetaKeys.CLIENT_CAPABILITIES}": {}}""", + ) as JsonObject + + RequestMeta(json).clientCapabilities shouldBe ClientCapabilities() + } + + @OptIn(ExperimentalMcpApi::class) + @Suppress("DEPRECATION") + @Test + fun `typed request metadata should reject malformed fields`() { + val malformedValues = listOf( + RequestMetaKeys.PROTOCOL_VERSION to "{}", + RequestMetaKeys.PROTOCOL_VERSION to "null", + RequestMetaKeys.CLIENT_INFO to "\"not-an-implementation\"", + RequestMetaKeys.CLIENT_INFO to "{\"name\":\"missing-version\"}", + RequestMetaKeys.CLIENT_INFO to "null", + RequestMetaKeys.CLIENT_CAPABILITIES to "\"not-capabilities\"", + RequestMetaKeys.CLIENT_CAPABILITIES to "[]", + RequestMetaKeys.CLIENT_CAPABILITIES to "null", + RequestMetaKeys.LOG_LEVEL to "\"verbose\"", + RequestMetaKeys.LOG_LEVEL to "7", + RequestMetaKeys.LOG_LEVEL to "null", + ) + + malformedValues.forEach { (key, value) -> + val json = Json.parseToJsonElement("""{"$key":$value}""") as JsonObject + val meta = RequestMeta(json) + + val failure = assertFailsWith { + when (key) { + RequestMetaKeys.PROTOCOL_VERSION -> meta.protocolVersion + RequestMetaKeys.CLIENT_INFO -> meta.clientInfo + RequestMetaKeys.CLIENT_CAPABILITIES -> meta.clientCapabilities + else -> meta.logLevel + } + } + assertTrue(failure.message.orEmpty().contains(key)) + } + } + + @OptIn(ExperimentalMcpApi::class) + @Test + fun `legacy requests should not gain request scoped metadata`() { + val request = ListToolsRequest() + + McpJson.encodeToString(request) shouldEqualJson """ + { + "method": "tools/list" + } + """.trimIndent() + assertNull(request.params?.meta?.protocolVersion) + } + + @Test + fun `should expose progress token from RequestMeta`() { + val stringTokenMeta = RequestMeta(buildJsonObject { put("progressToken", "sync-1") }) + val numberTokenMeta = RequestMeta(buildJsonObject { put("progressToken", 7) }) + val customMeta = RequestMeta( + buildJsonObject { + put("progressToken", "ignored") + put("source", "client") + }, + ) + + assertEquals(ProgressToken("sync-1"), stringTokenMeta.progressToken) + assertEquals(ProgressToken(7), numberTokenMeta.progressToken) + assertEquals("client", customMeta["source"]?.jsonPrimitive?.content) + } + + @Test + fun `should serialize BaseRequestParams with meta`() { + val params = BaseRequestParams( + meta = RequestMeta( + buildJsonObject { + put("progressToken", "base-42") + put("origin", "test-suite") + }, + ), + ) + + verifySerialization( + params, + McpJson, + """ + { + "_meta": { + "progressToken": "base-42", + "origin": "test-suite" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize BaseRequestParams with numeric progress token`() { + val json = """ + { + "_meta": { + "progressToken": 501, + "latencyMs": 12.5 + } + } + """.trimIndent() + + val params = verifyDeserialization(McpJson, json) + + val meta = params.meta + assertNotNull(meta) + assertEquals(ProgressToken(501), meta.progressToken) + assertEquals(12.5, meta["latencyMs"]?.jsonPrimitive?.double) + } + + @Test + fun `should serialize PaginatedRequestParams with cursor and meta`() { + val params = PaginatedRequestParams( + cursor = "cursor-1", + meta = RequestMeta(buildJsonObject { put("progressToken", "page-req") }), + ) + + verifySerialization( + params, + McpJson, + """ + { + "cursor": "cursor-1", + "_meta": { + "progressToken": "page-req" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize PaginatedRequestParams`() { + val json = """ + { + "cursor": "cursor-2", + "_meta": { + "progressToken": 99 + } + } + """.trimIndent() + + val params = verifyDeserialization(McpJson, json) + + assertEquals("cursor-2", params.cursor) + assertEquals(ProgressToken(99), params.meta?.progressToken) + } + + @Test + fun `should serialize CustomRequest with params`() { + val request = CustomRequest( + method = Method.Custom("workspace/sync"), + params = BaseRequestParams( + meta = RequestMeta(buildJsonObject { put("progressToken", "sync-req") }), + ), + ) + + val json = McpJson.encodeToString(request) + + json shouldEqualJson """ + { + "method": "workspace/sync", + "params": { + "_meta": { + "progressToken": "sync-req" + } + } + } + """.trimIndent() + } + + @Test + fun `should deserialize unknown method into CustomRequest`() { + val json = """ + { + "method": "extensions/customAction", + "params": { + "_meta": { + "progressToken": "custom-1" + } + } + } + """.trimIndent() + + val request = McpJson.decodeFromString(json) + + val custom = assertIs(request) + val method = custom.method + val customMethod = assertIs(method) + assertEquals("extensions/customAction", customMethod.value) + custom.params?.meta?.json + ?.get("progressToken") + ?.jsonPrimitive?.content shouldBe "custom-1" + } + + @Test + fun `should serialize EmptyResult with meta`() { + val result = EmptyResult( + meta = buildJsonObject { + put("processedBy", "worker-1") + }, + ) + + verifySerialization( + result, + McpJson, + """ + { + "_meta": { + "processedBy": "worker-1" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize EmptyResult without meta`() { + val json = "{}" + + val result = verifyDeserialization(McpJson, json) + + assertNull(result.meta) + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ResourcesTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ResourcesTest.kt new file mode 100644 index 0000000..e47a0f5 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ResourcesTest.kt @@ -0,0 +1,567 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.kotest.assertions.json.shouldEqualJson +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class ResourcesTest { + + @Test + fun `should serialize Resource with minimal fields`() { + val resource = Resource( + uri = "file:///workspace/README.md", + name = "README", + ) + + verifySerialization( + resource, + McpJson, + """ + { + "uri": "file:///workspace/README.md", + "name": "README" + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize Resource with all fields`() { + val resource = Resource( + uri = "file:///workspace/CHANGELOG.md", + name = "CHANGELOG", + description = "Changelog for recent releases", + mimeType = "text/markdown", + size = 4096, + title = "Project Changelog", + annotations = Annotations(priority = 0.8, audience = listOf(Role.Assistant)), + icons = listOf( + Icon(src = "https://example.com/changelog.png"), + ), + meta = buildJsonObject { put("etag", "abc123") }, + ) + + verifySerialization( + resource, + McpJson, + """ + { + "uri": "file:///workspace/CHANGELOG.md", + "name": "CHANGELOG", + "description": "Changelog for recent releases", + "mimeType": "text/markdown", + "size": 4096, + "title": "Project Changelog", + "annotations": { + "priority": 0.8, + "audience": ["assistant"] + }, + "icons": [ + {"src": "https://example.com/changelog.png"} + ], + "_meta": { + "etag": "abc123" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize Resource from JSON`() { + val json = """ + { + "uri": "file:///config/settings.json", + "name": "config-settings", + "description": "Configuration file", + "mimeType": "application/json", + "size": 1024, + "title": "Settings", + "annotations": { + "priority": 0.5 + }, + "icons": [ + {"src": "https://example.com/settings.png"} + ], + "_meta": { + "etag": "\"98765\"" + } + } + """.trimIndent() + + val resource = verifyDeserialization(McpJson, json) + + assertEquals("file:///config/settings.json", resource.uri) + assertEquals("config-settings", resource.name) + assertEquals("Configuration file", resource.description) + assertEquals("application/json", resource.mimeType) + assertEquals(1024, resource.size) + assertEquals("Settings", resource.title) + assertEquals(0.5, resource.annotations?.priority) + val meta = resource.meta + assertNotNull(meta) + assertEquals("\"98765\"", meta["etag"]?.jsonPrimitive?.content) + } + + @Test + fun `should serialize ResourceTemplate with meta`() { + val template = ResourceTemplate( + uriTemplate = "file:///workspace/{path}", + name = "workspace-file", + description = "Workspace file template", + mimeType = "text/plain", + title = "Workspace File", + annotations = Annotations( + priority = 0.6, + audience = listOf(Role.User, Role.Assistant), + ), + icons = listOf(Icon(src = "https://example.com/file.svg", theme = Icon.Theme.Light)), + meta = buildJsonObject { put("requiresAuth", true) }, + ) + + verifySerialization( + template, + McpJson, + """ + { + "uriTemplate": "file:///workspace/{path}", + "name": "workspace-file", + "description": "Workspace file template", + "mimeType": "text/plain", + "title": "Workspace File", + "annotations": { + "priority": 0.6, + "audience": ["user", "assistant"] + }, + "icons": [ + { + "src": "https://example.com/file.svg", + "theme": "light" + } + ], + "_meta": { + "requiresAuth": true + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize ResourceTemplate from JSON`() { + val json = """ + { + "uriTemplate": "db://users/{id}", + "name": "user-record", + "description": "Database record template", + "annotations": { + "priority": 1.0 + } + } + """.trimIndent() + + val template = verifyDeserialization(McpJson, json) + + assertEquals("db://users/{id}", template.uriTemplate) + assertEquals("user-record", template.name) + assertEquals("Database record template", template.description) + assertEquals(1.0, template.annotations?.priority) + assertNull(template.meta) + } + + @Test + fun `should serialize and deserialize ResourceTemplateReference`() { + val reference = ResourceTemplateReference(uri = "file:///workspace/{path}") + + val json = McpJson.encodeToString(reference) + + json shouldEqualJson """ + { + "type": "ref/resource", + "uri": "file:///workspace/{path}" + } + """.trimIndent() + + val decoded = verifyDeserialization(McpJson, json) + val templateReference = assertIs(decoded) + assertEquals("file:///workspace/{path}", templateReference.uri) + assertEquals(ReferenceType.ResourceTemplate, templateReference.type) + } + + @Test + fun `should serialize TextResourceContents`() { + val contents = TextResourceContents( + text = "Hello, MCP!", + uri = "file:///workspace/hello.txt", + mimeType = "text/plain", + meta = buildJsonObject { put("encoding", "utf-8") }, + ) + + verifySerialization( + contents, + McpJson, + """ + { + "text": "Hello, MCP!", + "uri": "file:///workspace/hello.txt", + "mimeType": "text/plain", + "_meta": { + "encoding": "utf-8" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize BlobResourceContents`() { + val json = """ + { + "blob": "YmluYXJ5ZGF0YQ==", + "uri": "file:///workspace/logo.png", + "mimeType": "image/png", + "_meta": { + "size": 128 + } + } + """.trimIndent() + + val contents = verifyDeserialization(McpJson, json) + val blobContents = assertIs(contents) + assertEquals("YmluYXJ5ZGF0YQ==", blobContents.blob) + assertEquals("file:///workspace/logo.png", blobContents.uri) + assertEquals("image/png", blobContents.mimeType) + assertEquals(128, blobContents.meta?.get("size")?.jsonPrimitive?.int) + } + + @Test + fun `should serialize UnknownResourceContents`() { + val contents = UnknownResourceContents( + uri = "custom://resource/42", + mimeType = "application/octet-stream", + ) + + verifySerialization( + contents, + McpJson, + """ + { + "uri": "custom://resource/42", + "mimeType": "application/octet-stream" + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ListResourcesRequest with cursor`() { + val request = ListResourcesRequest( + PaginatedRequestParams( + cursor = "cursor-1", + meta = RequestMeta(buildJsonObject { put("progressToken", "list-resources-1") }), + ), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "resources/list", + "params": { + "cursor": "cursor-1", + "_meta": { + "progressToken": "list-resources-1" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ListResourcesRequest without params`() { + val request = ListResourcesRequest() + + verifySerialization( + request, + McpJson, + """ + { + "method": "resources/list" + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ListResourcesResult`() { + val result = ListResourcesResult( + resources = listOf( + Resource(uri = "file:///workspace/README.md", name = "README"), + Resource(uri = "file:///workspace/CONTRIBUTING.md", name = "CONTRIBUTING"), + ), + nextCursor = "cursor-2", + meta = buildJsonObject { put("page", 1) }, + ) + + verifySerialization( + result, + McpJson, + """ + { + "resources": [ + {"uri": "file:///workspace/README.md", "name": "README"}, + {"uri": "file:///workspace/CONTRIBUTING.md", "name": "CONTRIBUTING"} + ], + "nextCursor": "cursor-2", + "_meta": { + "page": 1 + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize ListResourcesResult`() { + val json = """ + { + "resources": [ + { + "uri": "file:///workspace/docs/guide.md", + "name": "guide", + "description": "User guide" + } + ], + "nextCursor": "cursor-next", + "_meta": { + "page": 2 + } + } + """.trimIndent() + + val result = verifyDeserialization(McpJson, json) + + assertEquals("cursor-next", result.nextCursor) + val resources = result.resources + assertEquals(1, resources.size) + val resource = resources.first() + assertEquals("file:///workspace/docs/guide.md", resource.uri) + assertEquals("guide", resource.name) + assertEquals("User guide", resource.description) + assertEquals(2, result.meta?.get("page")?.jsonPrimitive?.int) + } + + @Test + fun `should serialize ReadResourceRequest`() { + val request = ReadResourceRequest( + ReadResourceRequestParams( + uri = "file:///workspace/notes.txt", + meta = RequestMeta(buildJsonObject { put("progressToken", "read-1") }), + ), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "resources/read", + "params": { + "uri": "file:///workspace/notes.txt", + "_meta": { + "progressToken": "read-1" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize ReadResourceResult with mixed contents`() { + val json = """ + { + "contents": [ + { + "text": "Section 1", + "uri": "file:///workspace/report.txt", + "mimeType": "text/plain" + }, + { + "blob": "aW1hZ2VEYXRh", + "uri": "file:///workspace/diagram.png", + "mimeType": "image/png" + } + ], + "_meta": { + "generatedAt": "2025-01-12T15:00:58Z" + } + } + """.trimIndent() + + val result = verifyDeserialization(McpJson, json) + + val contents = result.contents + assertEquals(2, contents.size) + assertIs(contents[0]) + assertIs(contents[1]) + val meta = result.meta + assertNotNull(meta) + assertEquals("2025-01-12T15:00:58Z", meta["generatedAt"]?.jsonPrimitive?.content) + } + + @Test + fun `should serialize SubscribeRequest`() { + val request = SubscribeRequest( + SubscribeRequestParams( + uri = "file:///workspace/todo.md", + meta = RequestMeta(buildJsonObject { put("progressToken", 42) }), + ), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "resources/subscribe", + "params": { + "uri": "file:///workspace/todo.md", + "_meta": { + "progressToken": 42 + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize UnsubscribeRequest`() { + val request = UnsubscribeRequest( + UnsubscribeRequestParams( + uri = "file:///workspace/todo.md", + meta = RequestMeta(buildJsonObject { put("progressToken", "unsubscribe-1") }), + ), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "resources/unsubscribe", + "params": { + "uri": "file:///workspace/todo.md", + "_meta": { + "progressToken": "unsubscribe-1" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ListResourceTemplatesRequest`() { + val request = ListResourceTemplatesRequest( + PaginatedRequestParams( + cursor = "template-1", + meta = RequestMeta(buildJsonObject { put("progressToken", "templates-1") }), + ), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "resources/templates/list", + "params": { + "cursor": "template-1", + "_meta": { + "progressToken": "templates-1" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ListResourceTemplatesResult`() { + val result = ListResourceTemplatesResult( + resourceTemplates = listOf( + ResourceTemplate(uriTemplate = "file:///workspace/{path}", name = "workspace-file"), + ), + nextCursor = "cursor-templates-2", + meta = buildJsonObject { put("page", 3) }, + ) + + verifySerialization( + result, + McpJson, + """ + { + "resourceTemplates": [ + { + "uriTemplate": "file:///workspace/{path}", + "name": "workspace-file" + } + ], + "nextCursor": "cursor-templates-2", + "_meta": { + "page": 3 + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize ListResourceTemplatesResult`() { + val json = """ + { + "resourceTemplates": [ + { + "uriTemplate": "db://records/{id}", + "name": "record", + "description": "Database record", + "icons": [ + {"src": "https://example.com/db.png"} + ] + } + ], + "nextCursor": "cursor-next", + "_meta": { + "page": 1 + } + } + """.trimIndent() + + val result = verifyDeserialization(McpJson, json) + + assertEquals("cursor-next", result.nextCursor) + val templates = result.resourceTemplates + assertEquals(1, templates.size) + val template = templates.first() + assertEquals("db://records/{id}", template.uriTemplate) + assertEquals("record", template.name) + assertEquals("Database record", template.description) + val icons = template.icons + assertNotNull(icons) + assertEquals("https://example.com/db.png", icons.first().src) + assertEquals(1, result.meta?.get("page")?.jsonPrimitive?.int) + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/RootsTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/RootsTest.kt new file mode 100644 index 0000000..2350c25 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/RootsTest.kt @@ -0,0 +1,180 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class RootsTest { + + @Test + fun `should serialize Root with minimal fields`() { + val root = Root(uri = "file:///workspace/project") + + verifySerialization( + root, + McpJson, + """ + { + "uri": "file:///workspace/project" + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize Root with name and meta`() { + val root = Root( + uri = "file:///workspace/docs", + name = "Docs", + meta = buildJsonObject { put("writable", true) }, + ) + + verifySerialization( + root, + McpJson, + """ + { + "uri": "file:///workspace/docs", + "name": "Docs", + "_meta": { + "writable": true + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize Root with validation`() { + val json = """ + { + "uri": "file:///workspace/code", + "name": "Code", + "_meta": { + "default": true + } + } + """.trimIndent() + + val root = verifyDeserialization(McpJson, json) + + assertEquals("file:///workspace/code", root.uri) + assertEquals("Code", root.name) + assertEquals(true, root.meta?.get("default")?.jsonPrimitive?.boolean) + } + + @Test + fun `should serialize ListRootsRequest with meta`() { + val request = ListRootsRequest( + BaseRequestParams( + meta = RequestMeta( + buildJsonObject { put("progressToken", "roots-list-1") }, + ), + ), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "roots/list", + "params": { + "_meta": { + "progressToken": "roots-list-1" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ListRootsRequest without params`() { + val request = ListRootsRequest() + + verifySerialization( + request, + McpJson, + """ + { + "method": "roots/list" + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ListRootsResult`() { + val result = ListRootsResult( + roots = listOf( + Root(uri = "file:///workspace/project", name = "Project"), + Root(uri = "file:///workspace/docs", name = "Docs"), + ), + meta = buildJsonObject { put("issuedAt", "2025-01-12T15:00:58Z") }, + ) + + verifySerialization( + result, + McpJson, + """ + { + "roots": [ + {"uri": "file:///workspace/project", "name": "Project"}, + {"uri": "file:///workspace/docs", "name": "Docs"} + ], + "_meta": { + "issuedAt": "2025-01-12T15:00:58Z" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize ListRootsResult`() { + val json = """ + { + "roots": [ + { + "uri": "file:///workspace/code", + "name": "Code" + } + ], + "_meta": { + "updatedBy": "client-1" + } + } + """.trimIndent() + + val result = verifyDeserialization(McpJson, json) + + val roots = result.roots + assertEquals(1, roots.size) + val root = roots.first() + assertEquals("file:///workspace/code", root.uri) + assertEquals("Code", root.name) + val meta = result.meta + assertNotNull(meta) + assertEquals("client-1", meta["updatedBy"]?.jsonPrimitive?.content) + } + + @Test + fun `should reject non file URI roots`() { + val json = """ + { + "uri": "https://example.com/root" + } + """.trimIndent() + + val exception = runCatching { McpJson.decodeFromString(json) }.exceptionOrNull() + assertNotNull(exception) + assertEquals(true, exception.message?.contains("Root URI must start with 'file://'")) + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/SamplingTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/SamplingTest.kt new file mode 100644 index 0000000..b7bc5cf --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/SamplingTest.kt @@ -0,0 +1,535 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.double +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertIs +import kotlin.test.assertNotNull + +class SamplingTest { + + @Serializable + private data class Holder( + @Serializable(with = SamplingContentSerializer::class) + val content: List, + ) + + private val dummyTool = Tool( + name = "get_weather", + description = "returns weather", + inputSchema = ToolSchema( + properties = buildJsonObject { }, + required = emptyList(), + ), + ) + + @Test + fun `should serialize ModelHint`() { + val hint = ModelHint(name = "claude-3-5-sonnet") + + verifySerialization( + hint, + McpJson, + """ + { + "name": "claude-3-5-sonnet" + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ModelPreferences with priorities`() { + val preferences = ModelPreferences( + hints = listOf(ModelHint(name = "haiku"), ModelHint(name = "openaichat")), + costPriority = 0.25, + speedPriority = 0.75, + intelligencePriority = 1.0, + ) + + verifySerialization( + preferences, + McpJson, + """ + { + "hints": [ + {"name": "haiku"}, + {"name": "openaichat"} + ], + "costPriority": 0.25, + "speedPriority": 0.75, + "intelligencePriority": 1.0 + } + """.trimIndent(), + ) + } + + @Test + fun `should reject ModelPreferences with invalid priority`() { + assertFailsWith { + ModelPreferences(costPriority = 1.5) + } + } + + @Test + fun `should serialize SamplingMessage`() { + val message = SamplingMessage( + role = Role.User, + content = listOf(TextContent(text = "Summarize the latest release.")), + ) + + verifySerialization( + message, + McpJson, + """ + { + "role": "user", + "content": { + "type": "text", + "text": "Summarize the latest release." + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize CreateMessageRequest with all fields`() { + val request = CreateMessageRequest( + CreateMessageRequestParams( + maxTokens = 512, + messages = listOf( + SamplingMessage( + role = Role.User, + content = listOf(TextContent(text = "You are a helpful assistant.")), + ), + SamplingMessage( + role = Role.User, + content = listOf(TextContent(text = "Provide a short summary.")), + ), + ), + modelPreferences = ModelPreferences( + hints = listOf(ModelHint(name = "claude")), + speedPriority = 0.6, + ), + systemPrompt = "Respond with concise bullet points.", + includeContext = IncludeContext.AllServers, + temperature = 0.8, + stopSequences = listOf("END"), + metadata = buildJsonObject { put("provider", "anthropic") }, + meta = RequestMeta(buildJsonObject { put("progressToken", "sample-1") }), + ), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "sampling/createMessage", + "params": { + "maxTokens": 512, + "messages": [ + { + "role": "user", + "content": { + "type": "text", + "text": "You are a helpful assistant." + } + }, + { + "role": "user", + "content": { + "type": "text", + "text": "Provide a short summary." + } + } + ], + "modelPreferences": { + "hints": [ + {"name": "claude"} + ], + "speedPriority": 0.6 + }, + "systemPrompt": "Respond with concise bullet points.", + "includeContext": "allServers", + "temperature": 0.8, + "stopSequences": ["END"], + "metadata": { + "provider": "anthropic" + }, + "_meta": { + "progressToken": "sample-1" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize CreateMessageRequest`() { + val json = """ + { + "method": "sampling/createMessage", + "params": { + "maxTokens": 256, + "messages": [ + { + "role": "user", + "content": { + "type": "text", + "text": "Draft a project update." + } + } + ], + "modelPreferences": { + "costPriority": 0.4 + }, + "includeContext": "thisServer", + "temperature": 1.1, + "stopSequences": ["\n\n"], + "metadata": { + "provider": "openai" + }, + "_meta": { + "progressToken": 42 + } + } + } + """.trimIndent() + + val request = verifyDeserialization(McpJson, json) + + assertEquals(Method.Defined.SamplingCreateMessage, request.method) + val params = request.params + assertEquals(256, params.maxTokens) + assertEquals(IncludeContext.ThisServer, params.includeContext) + assertEquals(ProgressToken(42), params.meta?.progressToken) + assertEquals("openai", params.metadata?.get("provider")?.jsonPrimitive?.content) + + val message = params.messages.first() + assertEquals(Role.User, message.role) + val content = assertIs(message.content.single()) + assertEquals("Draft a project update.", content.text) + + val preferences = assertNotNull(params.modelPreferences) + assertEquals(0.4, preferences.costPriority) + } + + @Test + fun `should serialize CreateMessageResult with stop reason`() { + val result = CreateMessageResult( + role = Role.Assistant, + content = TextContent(text = "Here is the requested update."), + model = "claude-3-5-sonnet", + stopReason = StopReason.MaxTokens, + meta = buildJsonObject { put("latencyMs", 850) }, + ) + + verifySerialization( + result, + McpJson, + """ + { + "role": "assistant", + "content": { + "type": "text", + "text": "Here is the requested update." + }, + "model": "claude-3-5-sonnet", + "stopReason": "maxTokens", + "_meta": { + "latencyMs": 850 + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize CreateMessageResult`() { + val json = """ + { + "role": "assistant", + "content": { + "type": "text", + "text": "Summary complete." + }, + "model": "gpt-4o", + "stopReason": "stopSequence", + "_meta": { + "latencyMs": 1200.5 + } + } + """.trimIndent() + + val result = verifyDeserialization(McpJson, json) + + assertEquals(Role.Assistant, result.role) + val text = assertIs(result.content.single()) + assertEquals("Summary complete.", text.text) + assertEquals("gpt-4o", result.model) + assertEquals(StopReason.StopSequence, result.stopReason) + val meta = result.meta + assertNotNull(meta) + assertEquals(1200.5, meta["latencyMs"]?.jsonPrimitive?.double) + } + + // ============================================================================ + // SamplingMessage shape (SEP-1577) + // ============================================================================ + + @Test + fun `SamplingMessage content is a list of SamplingMessageContent`() { + val m = SamplingMessage( + role = Role.User, + content = listOf(TextContent("hi")), + ) + m.content.size shouldBe 1 + (m.content[0] as TextContent).text shouldBe "hi" + } + + @Test + fun `SamplingMessage rejects empty content`() { + assertFailsWith { + SamplingMessage(role = Role.User, content = emptyList()) + } + } + + @Test + fun `SamplingMessage single-element content serialises as single object`() { + val m = SamplingMessage(role = Role.User, content = listOf(TextContent("hi"))) + val json = McpJson.encodeToString(SamplingMessage.serializer(), m) + check("""{"role":"user","content":""" in json) { "expected role/content prefix, got $json" } + check("\"type\":\"text\"" in json && "\"text\":\"hi\"" in json) { + "expected single-object content with text discriminator, got $json" + } + check("\"content\":[" !in json) { "expected single-object wire, but array form found: $json" } + } + + @Test + fun `SamplingMessage multi-element content serialises as array`() { + val m = SamplingMessage( + role = Role.Assistant, + content = listOf( + TextContent("Let me use a tool"), + ToolUseContent(id = "c1", name = "get_weather", input = JsonObject(emptyMap())), + ), + ) + val json = McpJson.encodeToString(SamplingMessage.serializer(), m) + check("\"content\":[" in json) { "expected array wire form, got $json" } + } + + @Test + fun `SamplingMessage _meta round-trips`() { + val meta = buildJsonObject { put("k", JsonPrimitive("v")) } + val m = SamplingMessage(role = Role.User, content = listOf(TextContent("hi")), meta = meta) + val json = McpJson.encodeToString(SamplingMessage.serializer(), m) + val decoded = McpJson.decodeFromString(SamplingMessage.serializer(), json) + decoded.meta shouldBe meta + } + + // ============================================================================ + // CreateMessageRequestParams: tools / toolChoice + StopReason.ToolUse + // ============================================================================ + + @Test + fun `CreateMessageRequestParams tools and toolChoice default to null`() { + val params = CreateMessageRequestParams(maxTokens = 100, messages = emptyList()) + params.tools shouldBe null + params.toolChoice shouldBe null + } + + @Test + fun `CreateMessageRequestParams round-trips tools and toolChoice`() { + val original = CreateMessageRequestParams( + maxTokens = 100, + messages = emptyList(), + tools = listOf(dummyTool), + toolChoice = ToolChoice(mode = ToolChoice.Mode.Required), + ) + val encoded = McpJson.encodeToString(CreateMessageRequestParams.serializer(), original) + val decoded = McpJson.decodeFromString(CreateMessageRequestParams.serializer(), encoded) + decoded.tools?.single()?.name shouldBe "get_weather" + decoded.toolChoice shouldBe ToolChoice(mode = ToolChoice.Mode.Required) + } + + @Test + fun `CreateMessageRequestParams task defaults to null and is omitted when encoding`() { + val params = CreateMessageRequestParams(maxTokens = 100, messages = emptyList()) + params.task shouldBe null + val encoded = McpJson.encodeToString(CreateMessageRequestParams.serializer(), params) + assertEquals(false, "\"task\"" in encoded) + } + + @Test + fun `CreateMessageRequestParams round-trips task`() { + val original = CreateMessageRequestParams( + maxTokens = 100, + messages = emptyList(), + task = TaskMetadata(ttl = 60_000L), + ) + val encoded = McpJson.encodeToString(CreateMessageRequestParams.serializer(), original) + val decoded = McpJson.decodeFromString(CreateMessageRequestParams.serializer(), encoded) + decoded.task shouldBe TaskMetadata(ttl = 60_000L) + } + + @Test + fun `StopReason ToolUse serialises as toolUse`() { + StopReason.ToolUse.value shouldBe "toolUse" + } + + // ============================================================================ + // CreateMessageResult shape (SEP-1577) + // ============================================================================ + + @Test + fun `CreateMessageResult single-block content serialises as single object`() { + val r = CreateMessageResult( + role = Role.Assistant, + content = TextContent("42"), + model = "test-model", + stopReason = StopReason.EndTurn, + ) + val json = McpJson.encodeToString(CreateMessageResult.serializer(), r) + check("\"content\":{" in json) { "expected single-object content wire form, got $json" } + check("\"content\":[" !in json) { "expected NOT to use array wire form for size-1, got $json" } + } + + @Test + fun `CreateMessageResult multi-block content with ToolUse stopReason round-trips`() { + val r = CreateMessageResult( + role = Role.Assistant, + content = listOf( + TextContent("Let me use a tool"), + ToolUseContent(id = "c1", name = "get_weather", input = JsonObject(emptyMap())), + ), + model = "test-model", + stopReason = StopReason.ToolUse, + ) + val json = McpJson.encodeToString(CreateMessageResult.serializer(), r) + val decoded = McpJson.decodeFromString(CreateMessageResult.serializer(), json) + decoded shouldBe r + decoded.stopReason shouldBe StopReason.ToolUse + decoded.content.size shouldBe 2 + } + + @Test + fun `CreateMessageResult pre-SEP single-object wire decodes correctly`() { + val json = """{"role":"assistant","content":{"type":"text","text":"hi"},"model":"m"}""" + val decoded = McpJson.decodeFromString(CreateMessageResult.serializer(), json) + decoded.content.size shouldBe 1 + (decoded.content[0] as TextContent).text shouldBe "hi" + } + + @Test + fun `CreateMessageResult rejects empty content`() { + assertFailsWith { + CreateMessageResult( + role = Role.Assistant, + content = emptyList(), + model = "test-model", + ) + } + } + + // ============================================================================ + // SamplingContentSerializer (single-or-array wire heuristic) + // ============================================================================ + + @Test + fun `SamplingContentSerializer decodes single object into list of one`() { + val json = """{"content":{"type":"text","text":"hi"}}""" + val h = McpJson.decodeFromString(Holder.serializer(), json) + h.content.size shouldBe 1 + h.content[0].shouldBeInstanceOf().text shouldBe "hi" + } + + @Test + fun `SamplingContentSerializer decodes array into list`() { + val json = """{"content":[{"type":"text","text":"a"},{"type":"text","text":"b"}]}""" + val h = McpJson.decodeFromString(Holder.serializer(), json) + h.content.size shouldBe 2 + } + + @Test + fun `SamplingContentSerializer encodes list of size one as a single object`() { + val h = Holder(content = listOf(TextContent("hi"))) + val json = McpJson.encodeToString(Holder.serializer(), h) + json shouldBe """{"content":{"text":"hi","type":"text"}}""" + } + + @Test + fun `SamplingContentSerializer encodes list of size two as an array`() { + val h = Holder(content = listOf(TextContent("a"), TextContent("b"))) + val json = McpJson.encodeToString(Holder.serializer(), h) + json shouldBe """{"content":[{"text":"a","type":"text"},{"text":"b","type":"text"}]}""" + } + + @Test + fun `SamplingContentSerializer encoding an empty list throws`() { + val h = Holder(content = emptyList()) + assertFailsWith { + McpJson.encodeToString(Holder.serializer(), h) + } + } + + @Test + fun `SamplingContentSerializer decoding an empty array throws`() { + val json = """{"content":[]}""" + assertFailsWith { + McpJson.decodeFromString(Holder.serializer(), json) + } + } + + // ============================================================================ + // ToolChoice + // ============================================================================ + + @Test + fun `ToolChoice round-trips auto mode`() { + val original = ToolChoice(mode = ToolChoice.Mode.Auto) + val json = McpJson.encodeToString(ToolChoice.serializer(), original) + json shouldBe """{"mode":"auto"}""" + McpJson.decodeFromString(ToolChoice.serializer(), json) shouldBe original + } + + @Test + fun `ToolChoice round-trips required mode`() { + val original = ToolChoice(mode = ToolChoice.Mode.Required) + McpJson.decodeFromString( + ToolChoice.serializer(), + McpJson.encodeToString(ToolChoice.serializer(), original), + ) shouldBe original + } + + @Test + fun `ToolChoice round-trips none mode`() { + val original = ToolChoice(mode = ToolChoice.Mode.None) + McpJson.decodeFromString( + ToolChoice.serializer(), + McpJson.encodeToString(ToolChoice.serializer(), original), + ) shouldBe original + } + + @Test + fun `ToolChoice absent mode serialises as empty object and deserialises to null mode`() { + val original = ToolChoice() + val json = McpJson.encodeToString(ToolChoice.serializer(), original) + json shouldBe """{}""" + McpJson.decodeFromString(ToolChoice.serializer(), json) shouldBe ToolChoice(mode = null) + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/TasksTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/TasksTest.kt new file mode 100644 index 0000000..14bd248 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/TasksTest.kt @@ -0,0 +1,802 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.test.Test + +class TasksTest { + + // ======================================================================== + // Task + // ======================================================================== + + @Test + fun `should serialize Task with minimal fields`() { + val task = Task( + taskId = "task-1", + status = TaskStatus.Working, + createdAt = "2025-01-01T00:00:00Z", + lastUpdatedAt = "2025-01-01T00:00:00Z", + ttl = null, + ) + + verifySerialization( + task, + McpJson, + """ + { + "taskId": "task-1", + "status": "working", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:00:00Z" + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize Task with all fields`() { + val task = Task( + taskId = "task-2", + status = TaskStatus.Completed, + statusMessage = "Processing complete", + createdAt = "2025-01-01T00:00:00Z", + lastUpdatedAt = "2025-01-01T00:01:00Z", + ttl = 60000, + pollInterval = 5000, + ) + + verifySerialization( + task, + McpJson, + """ + { + "taskId": "task-2", + "status": "completed", + "statusMessage": "Processing complete", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:01:00Z", + "ttl": 60000, + "pollInterval": 5000 + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize Task and verify TaskFields contract`() { + val json = """ + { + "taskId": "task-3", + "status": "failed", + "statusMessage": "Connection lost", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:02:00Z", + "ttl": 30000, + "pollInterval": 1000 + } + """.trimIndent() + + val task = verifyDeserialization(McpJson, json) + task.shouldBeInstanceOf() + task.taskId shouldBe "task-3" + task.status shouldBe TaskStatus.Failed + task.statusMessage shouldBe "Connection lost" + task.createdAt shouldBe "2025-01-01T00:00:00Z" + task.lastUpdatedAt shouldBe "2025-01-01T00:02:00Z" + task.ttl shouldBe 30000L + task.pollInterval shouldBe 1000L + } + + // ======================================================================== + // TaskStatus + // ======================================================================== + + @Test + fun `should serialize all TaskStatus values`() { + verifySerialization(TaskStatus.Working, McpJson, "\"working\"") + verifySerialization(TaskStatus.InputRequired, McpJson, "\"input_required\"") + verifySerialization(TaskStatus.Completed, McpJson, "\"completed\"") + verifySerialization(TaskStatus.Failed, McpJson, "\"failed\"") + verifySerialization(TaskStatus.Cancelled, McpJson, "\"cancelled\"") + } + + // ======================================================================== + // TaskMetadata + // ======================================================================== + + @Test + fun `should serialize TaskMetadata with ttl`() { + val metadata = TaskMetadata(ttl = 120000) + + verifySerialization( + metadata, + McpJson, + """ + { + "ttl": 120000 + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize TaskMetadata without ttl`() { + val metadata = TaskMetadata() + + verifySerialization( + metadata, + McpJson, + "{}", + ) + } + + // ======================================================================== + // RelatedTaskMetadata + // ======================================================================== + + @Test + fun `should serialize RelatedTaskMetadata`() { + val related = RelatedTaskMetadata(taskId = "task-42") + + verifySerialization( + related, + McpJson, + """ + { + "taskId": "task-42" + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize RequestMeta with RelatedTaskMetadata`() { + val json = """ + { + "io.modelcontextprotocol/related-task": { + "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840" + } + } + """.trimIndent() + + val meta = McpJson.decodeFromString(json) + val related = meta.relatedTask + related.shouldNotBeNull() + related.taskId shouldBe "786512e2-9e0d-44bd-8f29-789f320fe840" + } + + // ======================================================================== + // CreateTaskResult + // ======================================================================== + + @Test + fun `should serialize CreateTaskResult`() { + val result = CreateTaskResult( + task = Task( + taskId = "task-1", + status = TaskStatus.Working, + createdAt = "2025-01-01T00:00:00Z", + lastUpdatedAt = "2025-01-01T00:00:00Z", + ttl = null, + ), + ) + + verifySerialization( + result, + McpJson, + """ + { + "task": { + "taskId": "task-1", + "status": "working", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:00:00Z" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize CreateTaskResult with meta`() { + val result = CreateTaskResult( + task = Task( + taskId = "task-1", + status = TaskStatus.Working, + createdAt = "2025-01-01T00:00:00Z", + lastUpdatedAt = "2025-01-01T00:00:00Z", + ttl = 60000, + ), + meta = buildJsonObject { put("trace", "abc") }, + ) + + verifySerialization( + result, + McpJson, + """ + { + "task": { + "taskId": "task-1", + "status": "working", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:00:00Z", + "ttl": 60000 + }, + "_meta": { + "trace": "abc" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize CreateTaskResult`() { + val json = """ + { + "task": { + "taskId": "task-5", + "status": "input_required", + "statusMessage": "Need more data", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:01:00Z", + "pollInterval": 2000 + } + } + """.trimIndent() + + val result = verifyDeserialization(McpJson, json) + result.task.taskId shouldBe "task-5" + result.task.status shouldBe TaskStatus.InputRequired + result.task.statusMessage shouldBe "Need more data" + result.task.pollInterval shouldBe 2000L + result.task.ttl.shouldBeNull() + result.meta.shouldBeNull() + } + + // ======================================================================== + // GetTaskRequest + // ======================================================================== + + @Test + fun `should serialize GetTaskRequest`() { + val request = GetTaskRequest( + GetTaskRequestParams(taskId = "task-10"), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "tasks/get", + "params": { + "taskId": "task-10" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize GetTaskRequest with meta`() { + val request = GetTaskRequest( + GetTaskRequestParams( + taskId = "task-10", + meta = RequestMeta( + buildJsonObject { put("progressToken", "pt-1") }, + ), + ), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "tasks/get", + "params": { + "taskId": "task-10", + "_meta": { + "progressToken": "pt-1" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize GetTaskRequest`() { + val json = """ + { + "method": "tasks/get", + "params": { + "taskId": "task-11" + } + } + """.trimIndent() + + val request = verifyDeserialization(McpJson, json) + request.method shouldBe Method.Defined.TasksGet + request.taskId shouldBe "task-11" + request.meta.shouldBeNull() + } + + // ======================================================================== + // GetTaskResult + // ======================================================================== + + @Test + fun `should serialize GetTaskResult with all fields`() { + val result = GetTaskResult( + taskId = "task-20", + status = TaskStatus.Completed, + statusMessage = "Done", + createdAt = "2025-01-01T00:00:00Z", + lastUpdatedAt = "2025-01-01T00:05:00Z", + ttl = 300000, + pollInterval = 10000, + meta = buildJsonObject { put("server", "main") }, + ) + + verifySerialization( + result, + McpJson, + """ + { + "taskId": "task-20", + "status": "completed", + "statusMessage": "Done", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:05:00Z", + "ttl": 300000, + "pollInterval": 10000, + "_meta": { + "server": "main" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize GetTaskResult and verify TaskFields contract`() { + val json = """ + { + "taskId": "task-21", + "status": "working", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:00:30Z" + } + """.trimIndent() + + val result = verifyDeserialization(McpJson, json) + result.shouldBeInstanceOf() + result.taskId shouldBe "task-21" + result.status shouldBe TaskStatus.Working + result.statusMessage.shouldBeNull() + result.ttl.shouldBeNull() + result.pollInterval.shouldBeNull() + result.meta.shouldBeNull() + } + + // ======================================================================== + // GetTaskPayloadRequest + // ======================================================================== + + @Test + fun `should serialize GetTaskPayloadRequest`() { + val request = GetTaskPayloadRequest( + GetTaskPayloadRequestParams(taskId = "task-30"), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "tasks/result", + "params": { + "taskId": "task-30" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize GetTaskPayloadRequest`() { + val json = """ + { + "method": "tasks/result", + "params": { + "taskId": "task-31" + } + } + """.trimIndent() + + val request = verifyDeserialization(McpJson, json) + request.method shouldBe Method.Defined.TasksResult + request.taskId shouldBe "task-31" + request.meta.shouldBeNull() + } + + // ======================================================================== + // GetTaskPayloadResult + // ======================================================================== + + @Test + fun `should serialize GetTaskPayloadResult with meta`() { + val result = GetTaskPayloadResult( + buildJsonObject { + put("_meta", buildJsonObject { put("origin", "task-30") }) + }, + ) + + verifySerialization( + result, + McpJson, + """ + { + "_meta": { + "origin": "task-30" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize GetTaskPayloadResult without meta`() { + val result = GetTaskPayloadResult(buildJsonObject { }) + + verifySerialization(result, McpJson, "{}") + } + + @Test + fun `should preserve arbitrary payload fields in GetTaskPayloadResult`() { + val json = """ + { + "_meta": { + "origin": "task-42" + }, + "content": [ + { + "type": "text", + "text": "hello" + } + ], + "isError": false + } + """.trimIndent() + + val result = verifyDeserialization(McpJson, json) + result.meta.shouldNotBeNull() + result.meta?.get("origin")?.jsonPrimitive?.content shouldBe "task-42" + result["content"].shouldNotBeNull() + result["isError"].shouldNotBeNull() + } + + // ======================================================================== + // ListTasksRequest + // ======================================================================== + + @Test + fun `should serialize ListTasksRequest without params`() { + val request = ListTasksRequest() + + verifySerialization( + request, + McpJson, + """ + { + "method": "tasks/list" + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ListTasksRequest with cursor`() { + val request = ListTasksRequest( + params = PaginatedRequestParams(cursor = "page-2"), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "tasks/list", + "params": { + "cursor": "page-2" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize ListTasksRequest`() { + val json = """ + { + "method": "tasks/list", + "params": { + "cursor": "next-page" + } + } + """.trimIndent() + + val request = verifyDeserialization(McpJson, json) + request.method shouldBe Method.Defined.TasksList + request.cursor shouldBe "next-page" + } + + // ======================================================================== + // ListTasksResult + // ======================================================================== + + @Test + fun `should serialize ListTasksResult with empty list`() { + val result = ListTasksResult(tasks = emptyList()) + + verifySerialization( + result, + McpJson, + """ + { + "tasks": [] + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ListTasksResult with tasks and pagination`() { + val result = ListTasksResult( + tasks = listOf( + Task( + taskId = "task-1", + status = TaskStatus.Working, + createdAt = "2025-01-01T00:00:00Z", + lastUpdatedAt = "2025-01-01T00:00:00Z", + ttl = null, + ), + Task( + taskId = "task-2", + status = TaskStatus.Completed, + createdAt = "2025-01-01T00:00:00Z", + lastUpdatedAt = "2025-01-01T00:01:00Z", + ttl = 60000, + ), + ), + nextCursor = "cursor-abc", + ) + + verifySerialization( + result, + McpJson, + """ + { + "tasks": [ + { + "taskId": "task-1", + "status": "working", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:00:00Z" + }, + { + "taskId": "task-2", + "status": "completed", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:01:00Z", + "ttl": 60000 + } + ], + "nextCursor": "cursor-abc" + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize ListTasksResult`() { + val json = """ + { + "tasks": [ + { + "taskId": "task-99", + "status": "cancelled", + "statusMessage": "User cancelled", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:03:00Z" + } + ] + } + """.trimIndent() + + val result = verifyDeserialization(McpJson, json) + result.tasks.size shouldBe 1 + result.tasks[0].taskId shouldBe "task-99" + result.tasks[0].status shouldBe TaskStatus.Cancelled + result.tasks[0].statusMessage shouldBe "User cancelled" + result.tasks[0].ttl.shouldBeNull() + result.nextCursor.shouldBeNull() + } + + // ======================================================================== + // CancelTaskRequest + // ======================================================================== + + @Test + fun `should serialize CancelTaskRequest`() { + val request = CancelTaskRequest( + CancelTaskRequestParams(taskId = "task-40"), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "tasks/cancel", + "params": { + "taskId": "task-40" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize CancelTaskRequest`() { + val json = """ + { + "method": "tasks/cancel", + "params": { + "taskId": "task-41" + } + } + """.trimIndent() + + val request = verifyDeserialization(McpJson, json) + request.method shouldBe Method.Defined.TasksCancel + request.taskId shouldBe "task-41" + request.meta.shouldBeNull() + } + + // ======================================================================== + // CancelTaskResult + // ======================================================================== + + @Test + fun `should serialize CancelTaskResult`() { + val result = CancelTaskResult( + taskId = "task-40", + status = TaskStatus.Cancelled, + statusMessage = "Cancelled by user", + createdAt = "2025-01-01T00:00:00Z", + lastUpdatedAt = "2025-01-01T00:02:00Z", + ttl = null, + ) + + verifySerialization( + result, + McpJson, + """ + { + "taskId": "task-40", + "status": "cancelled", + "statusMessage": "Cancelled by user", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:02:00Z" + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize CancelTaskResult and verify TaskFields contract`() { + val json = """ + { + "taskId": "task-42", + "status": "cancelled", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:04:00Z", + "ttl": 120000, + "pollInterval": 3000 + } + """.trimIndent() + + val result = verifyDeserialization(McpJson, json) + result.shouldBeInstanceOf() + result.taskId shouldBe "task-42" + result.status shouldBe TaskStatus.Cancelled + result.statusMessage.shouldBeNull() + result.ttl shouldBe 120000L + result.pollInterval shouldBe 3000L + } + + // ======================================================================== + // Deserialization with unknown fields (ignoreUnknownKeys) + // ======================================================================== + + @Test + fun `should deserialize Task ignoring unknown fields`() { + val json = """ + { + "taskId": "task-100", + "status": "working", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:00:00Z", + "unknownField": "should be ignored" + } + """.trimIndent() + + val task = McpJson.decodeFromString(json) + task.taskId shouldBe "task-100" + task.status shouldBe TaskStatus.Working + task.ttl.shouldBeNull() + } + + // ======================================================================== + // RequestMeta.relatedTask accessor + // ======================================================================== + + @Test + fun `should read relatedTask from RequestMeta`() { + val meta = RequestMeta( + buildJsonObject { + put( + RELATED_TASK_META_KEY, + buildJsonObject { put("taskId", "task-50") }, + ) + }, + ) + + val related = meta.relatedTask + related.shouldNotBeNull() + related.taskId shouldBe "task-50" + } + + @Test + fun `should return null relatedTask when key is absent`() { + val meta = RequestMeta( + buildJsonObject { put("progressToken", "pt-1") }, + ) + + meta.relatedTask.shouldBeNull() + } + + @Test + fun `GetTaskResult cast to CancelTaskResult via generic does not throw due to type erasure`() { + val result: RequestResult = GetTaskResult( + taskId = "task-1", + status = TaskStatus.Cancelled, + createdAt = "2025-01-01T00:00:00Z", + lastUpdatedAt = "2025-01-01T00:01:00Z", + ttl = null, + ) + + // Simulates what Protocol.request() does: + // the cast goes through a generic type parameter, so JVM erases T to RequestResult + fun uncheckedCast(value: RequestResult): T { + @Suppress("UNCHECKED_CAST") + return value as T + } + + val cancelResult: CancelTaskResult = uncheckedCast(result) + + cancelResult.taskId shouldBe "task-1" + cancelResult.status shouldBe TaskStatus.Cancelled + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ToolsTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ToolsTest.kt new file mode 100644 index 0000000..358866a --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ToolsTest.kt @@ -0,0 +1,850 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull + +class ToolsTest { + + @Test + fun `should serialize Tool with minimal fields`() { + val tool = Tool( + name = "search", + inputSchema = ToolSchema(), + ) + + verifySerialization( + tool, + McpJson, + """ + { + "name": "search", + "inputSchema": { + "type": "object" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize Tool with annotations and schemas`() { + val inputSchema = ToolSchema( + properties = buildJsonObject { + put( + "query", + buildJsonObject { + put("type", "string") + put("description", "Search query") + }, + ) + }, + required = listOf("query"), + ) + val outputSchema = ToolSchema( + properties = buildJsonObject { + put( + "results", + buildJsonObject { + put("type", "array") + }, + ) + }, + ) + val tool = Tool( + name = "web-search", + inputSchema = inputSchema, + description = "Search the web for information", + outputSchema = outputSchema, + title = "Web Search", + annotations = ToolAnnotations( + title = "Web Search (Preferred)", + readOnlyHint = true, + destructiveHint = false, + idempotentHint = true, + openWorldHint = true, + ), + icons = listOf(Icon(src = "https://example.com/search.png")), + meta = buildJsonObject { put("category", "search") }, + ) + + verifySerialization( + tool, + McpJson, + """ + { + "name": "web-search", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Search query" + } + }, + "required": ["query"] + }, + "description": "Search the web for information", + "outputSchema": { + "type": "object", + "properties": { + "results": { + "type": "array" + } + } + }, + "title": "Web Search", + "annotations": { + "title": "Web Search (Preferred)", + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + }, + "icons": [ + {"src": "https://example.com/search.png"} + ], + "_meta": { + "category": "search" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize Tool from JSON`() { + val json = """ + { + "name": "translate", + "inputSchema": { + "type": "object", + "properties": { + "text": {"type": "string"}, + "targetLanguage": {"type": "string"} + }, + "required": ["text", "targetLanguage"] + }, + "annotations": { + "title": "Translate Text", + "readOnlyHint": true + }, + "_meta": { + "category": "language" + } + } + """.trimIndent() + + val tool = verifyDeserialization(McpJson, json) + + assertEquals("translate", tool.name) + assertEquals("Translate Text", tool.annotations?.title) + assertEquals(true, tool.annotations?.readOnlyHint) + val schema = tool.inputSchema + val properties = schema.properties + assertNotNull(properties) + assertNotNull(properties["text"]) + assertEquals(listOf("text", "targetLanguage"), schema.required) + assertEquals("language", tool.meta?.get("category")?.jsonPrimitive?.content) + } + + @Test + fun `should serialize ToolSchema with schema field`() { + val tool = Tool( + name = "typed-tool", + inputSchema = ToolSchema( + schema = "https://json-schema.org/draft/2020-12/schema", + properties = buildJsonObject { + put("name", buildJsonObject { put("type", "string") }) + }, + ), + ) + + verifySerialization( + tool, + McpJson, + $$""" + { + "name": "typed-tool", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize ToolSchema with schema field`() { + val json = $$""" + { + "name": "typed-tool", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + } + } + """.trimIndent() + + val tool = verifyDeserialization(McpJson, json) + + assertEquals("https://json-schema.org/draft/2020-12/schema", tool.inputSchema.schema) + assertNotNull(tool.inputSchema.properties) + } + + @Test + fun `should serialize ToolSchema with defs`() { + val tool = toolWithDefs() + + verifySerialization(tool, McpJson, toolWithDefsJson()) + } + + @Test + fun `should deserialize ToolSchema with defs`() { + val json = $$""" + { + "name": "create-page", + "inputSchema": { + "type": "object", + "$defs": { + "parentRequest": { + "type": "object", + "properties": { + "page_id": { + "type": "string" + } + } + } + }, + "properties": { + "parent": { + "$ref": "#/$defs/parentRequest" + } + }, + "required": ["parent"] + } + } + """.trimIndent() + + val tool = verifyDeserialization(McpJson, json) + + val schema = tool.inputSchema + val defs = schema.defs + assertNotNull(defs) + val parentRequest = defs["parentRequest"]?.jsonObject + assertNotNull(parentRequest) + assertEquals("object", parentRequest["type"]?.jsonPrimitive?.content) + assertEquals( + $$"#/$defs/parentRequest", + schema.properties?.get("parent")?.jsonObject?.get($$"$ref")?.jsonPrimitive?.content, + ) + assertEquals(listOf("parent"), schema.required) + } + + @Test + fun `should serialize CallToolRequest with arguments`() { + val request = CallToolRequest( + CallToolRequestParams( + name = "web-search", + arguments = buildJsonObject { put("query", "MCP protocol") }, + meta = RequestMeta( + buildJsonObject { put("progressToken", "call-1") }, + ), + ), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "tools/call", + "params": { + "name": "web-search", + "arguments": { + "query": "MCP protocol" + }, + "_meta": { + "progressToken": "call-1" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize CallToolRequest`() { + val json = """ + { + "method": "tools/call", + "params": { + "name": "analyze-code", + "arguments": { + "path": "src/main.kt" + }, + "_meta": { + "progressToken": 77 + } + } + } + """.trimIndent() + + val request = verifyDeserialization(McpJson, json) + + assertEquals(Method.Defined.ToolsCall, request.method) + val params = request.params + assertEquals("analyze-code", params.name) + assertEquals("src/main.kt", params.arguments?.get("path")?.jsonPrimitive?.content) + assertEquals(ProgressToken(77), params.meta?.progressToken) + } + + @Test + fun `should serialize CallToolRequest with task augmentation`() { + val request = CallToolRequest( + CallToolRequestParams( + name = "long-running", + arguments = buildJsonObject { put("query", "MCP protocol") }, + task = TaskMetadata(ttl = 60_000L), + ), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "tools/call", + "params": { + "name": "long-running", + "arguments": { + "query": "MCP protocol" + }, + "task": { + "ttl": 60000 + } + } + } + """.trimIndent(), + ) + + assertEquals(TaskMetadata(ttl = 60_000L), request.task) + } + + @Test + fun `should omit task from CallToolRequest when null`() { + val request = CallToolRequest(CallToolRequestParams(name = "plain")) + + assertEquals(null, request.task) + verifySerialization( + request, + McpJson, + """ + { + "method": "tools/call", + "params": { + "name": "plain" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize CallToolResult with structured content`() { + val result = CallToolResult( + content = listOf( + TextContent(text = "Found 3 relevant documents."), + ), + isError = false, + structuredContent = buildJsonObject { + put("count", 3) + put("items", buildJsonObject { put("first", "doc.md") }) + }, + meta = buildJsonObject { put("elapsedMs", 1200) }, + ) + + verifySerialization( + result, + McpJson, + """ + { + "content": [ + { + "type": "text", + "text": "Found 3 relevant documents." + } + ], + "isError": false, + "structuredContent": { + "count": 3, + "items": { + "first": "doc.md" + } + }, + "_meta": { + "elapsedMs": 1200 + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize CallToolResult`() { + val json = """ + { + "content": [ + { + "type": "text", + "text": "Unable to reach server." + } + ], + "isError": true, + "structuredContent": { + "errorCode": "NETWORK", + "retryable": true + }, + "_meta": { + "requestId": "req-9" + } + } + """.trimIndent() + + val result = verifyDeserialization(McpJson, json) + + assertEquals(true, result.isError) + val text = assertIs(result.content.first()) + assertEquals("Unable to reach server.", text.text) + val structured = result.structuredContent + assertNotNull(structured) + assertEquals("NETWORK", structured["errorCode"]?.jsonPrimitive?.content) + assertEquals("req-9", result.meta?.get("requestId")?.jsonPrimitive?.content) + } + + @Test + fun `should serialize ListToolsRequest with cursor`() { + val request = ListToolsRequest( + PaginatedRequestParams( + cursor = "cursor-1", + meta = RequestMeta(buildJsonObject { put("progressToken", "tools-list-1") }), + ), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "tools/list", + "params": { + "cursor": "cursor-1", + "_meta": { + "progressToken": "tools-list-1" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ListToolsRequest without params`() { + val request = ListToolsRequest() + + verifySerialization( + request, + McpJson, + """ + { + "method": "tools/list" + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ListToolsResult`() { + val result = ListToolsResult( + tools = listOf( + Tool(name = "search", inputSchema = ToolSchema()), + Tool(name = "summarize", inputSchema = ToolSchema()), + ), + nextCursor = "cursor-2", + meta = buildJsonObject { put("page", 1) }, + ) + + verifySerialization( + result, + McpJson, + """ + { + "tools": [ + { + "name": "search", + "inputSchema": { + "type": "object" + } + }, + { + "name": "summarize", + "inputSchema": { + "type": "object" + } + } + ], + "nextCursor": "cursor-2", + "_meta": { + "page": 1 + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize ListToolsResult`() { + val json = """ + { + "tools": [ + { + "name": "search", + "inputSchema": { + "type": "object" + }, + "description": "Search the workspace" + } + ], + "nextCursor": "cursor-next", + "_meta": { + "page": 3 + } + } + """.trimIndent() + + val result = verifyDeserialization(McpJson, json) + + assertEquals("cursor-next", result.nextCursor) + val tools = result.tools + assertEquals(1, tools.size) + val tool = tools.first() + assertEquals("search", tool.name) + assertEquals("Search the workspace", tool.description) + assertEquals("object", tool.inputSchema.type) + assertEquals(3, result.meta?.get("page")?.jsonPrimitive?.int) + } + + @Test + fun `should build success CallToolResult with text content`() { + val meta = buildJsonObject { put("source", "toolkit") } + + val result = CallToolResult.success("Operation complete", meta) + + assertEquals(false, result.isError) + val text = assertIs(result.content.single()) + assertEquals("Operation complete", text.text) + assertEquals("toolkit", result.meta?.get("source")?.jsonPrimitive?.content) + assertEquals(null, result.structuredContent) + } + + @Test + fun `should build error CallToolResult with text content`() { + val meta = buildJsonObject { put("code", "ERR42") } + + val result = CallToolResult.error("Failed to connect", meta) + + assertEquals(true, result.isError) + val text = assertIs(result.content.single()) + assertEquals("Failed to connect", text.text) + assertEquals("ERR42", result.meta?.get("code")?.jsonPrimitive?.content) + assertEquals(null, result.structuredContent) + } + + @Test + fun `should serialize ToolExecution with taskSupport`() { + val execution = ToolExecution(taskSupport = TaskSupport.Required) + + verifySerialization( + execution, + McpJson, + """ + { + "taskSupport": "required" + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ToolExecution without taskSupport`() { + val execution = ToolExecution() + + verifySerialization( + execution, + McpJson, + "{}", + ) + } + + @Test + fun `should deserialize ToolExecution with taskSupport`() { + val json = """ + { + "taskSupport": "optional" + } + """.trimIndent() + + val execution = verifyDeserialization(McpJson, json) + + assertEquals(TaskSupport.Optional, execution.taskSupport) + } + + @Test + fun `should deserialize ToolExecution without taskSupport`() { + val json = "{}" + + val execution = verifyDeserialization(McpJson, json) + + assertEquals(null, execution.taskSupport) + } + + @Test + fun `should serialize Tool with execution`() { + val tool = Tool( + name = "long-running-task", + inputSchema = ToolSchema(), + description = "A tool that supports task-augmented execution", + execution = ToolExecution(taskSupport = TaskSupport.Required), + ) + + verifySerialization( + tool, + McpJson, + """ + { + "name": "long-running-task", + "inputSchema": { + "type": "object" + }, + "description": "A tool that supports task-augmented execution", + "execution": { + "taskSupport": "required" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize Tool with execution`() { + val json = """ + { + "name": "long-running-task", + "inputSchema": { + "type": "object" + }, + "execution": { + "taskSupport": "forbidden" + } + } + """.trimIndent() + + val tool = verifyDeserialization(McpJson, json) + + assertEquals("long-running-task", tool.name) + assertNotNull(tool.execution) + assertEquals(TaskSupport.Forbidden, tool.execution.taskSupport) + } + + @Test + fun `should serialize tool schema type even when defaults disabled`() { + val tool = weatherTool() + + val customJson = Json(from = McpJson) { + encodeDefaults = false + } + val encoded = customJson.encodeToString(Tool.serializer(), tool) + val element = Json.parseToJsonElement(encoded).jsonObject + + val inputSchema = element["inputSchema"]?.jsonObject + val outputSchema = element["outputSchema"]?.jsonObject + + assertNotNull(inputSchema) + assertEquals("object", inputSchema["type"]?.jsonPrimitive?.content) + assertNotNull(outputSchema) + assertEquals("object", outputSchema["type"]?.jsonPrimitive?.content) + } + + @Test + fun `should deserialize complex tool definition`() { + val expected = weatherTool() + val actual = verifyDeserialization(McpJson, weatherToolJson()) + + assertEquals(expected, actual) + } + + private fun weatherTool(): Tool = Tool( + name = "get_weather", + title = "Get weather", + description = "Get the current weather in a given location", + inputSchema = ToolSchema( + properties = buildJsonObject { + put( + "location", + buildJsonObject { + put("type", "string") + put("description", "The city and state, e.g. San Francisco, CA") + }, + ) + }, + required = listOf("location"), + ), + outputSchema = ToolSchema( + properties = buildJsonObject { + put( + "temperature", + buildJsonObject { + put("type", "number") + put("description", "Temperature in celsius") + }, + ) + put( + "conditions", + buildJsonObject { + put("type", "string") + put("description", "Weather conditions description") + }, + ) + put( + "humidity", + buildJsonObject { + put("type", "number") + put("description", "Humidity percentage") + }, + ) + }, + required = listOf("temperature", "conditions", "humidity"), + ), + meta = buildJsonObject { put("_for_test_only", true) }, + ) + + private fun weatherToolJson(): String = """ + { + "name": "get_weather", + "title": "Get weather", + "description": "Get the current weather in a given location", + "inputSchema": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA" + } + }, + "required": ["location"] + }, + "outputSchema": { + "type": "object", + "properties": { + "temperature": { + "type": "number", + "description": "Temperature in celsius" + }, + "conditions": { + "type": "string", + "description": "Weather conditions description" + }, + "humidity": { + "type": "number", + "description": "Humidity percentage" + } + }, + "required": ["temperature", "conditions", "humidity"] + }, + "_meta": { + "_for_test_only": true + } + } + """.trimIndent() + + private fun toolWithDefs(): Tool = Tool( + name = "create-page", + inputSchema = toolSchemaWithDefs(), + ) + + private fun toolSchemaWithDefs(): ToolSchema = ToolSchema( + properties = buildJsonObject { + put( + "parent", + buildJsonObject { + put($$"$ref", $$"#/$defs/parentRequest") + }, + ) + }, + required = listOf("parent"), + defs = buildJsonObject { + put( + "parentRequest", + buildJsonObject { + put("type", "object") + put( + "properties", + buildJsonObject { + put( + "page_id", + buildJsonObject { + put("type", "string") + }, + ) + }, + ) + }, + ) + }, + ) + + private fun toolWithDefsJson(): String = $$""" + { + "name": "create-page", + "inputSchema": { + "type": "object", + "$defs": { + "parentRequest": { + "type": "object", + "properties": { + "page_id": { + "type": "string" + } + } + } + }, + "properties": { + "parent": { + "$ref": "#/$defs/parentRequest" + } + }, + "required": ["parent"] + } + } + """.trimIndent() +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CapabilitiesDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CapabilitiesDslTest.kt new file mode 100644 index 0000000..25d9ee6 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CapabilitiesDslTest.kt @@ -0,0 +1,183 @@ +package io.modelcontextprotocol.kotlin.sdk.types.dsl + +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject +import io.modelcontextprotocol.kotlin.sdk.types.buildInitializeRequest +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.test.Test + +/** + * Tests for ClientCapabilities DSL builder. + * + * Verifies that capabilities can be constructed via DSL within InitializeRequest, + * covering minimal (empty), full (all fields), and variant patterns. + */ +@OptIn(ExperimentalMcpApi::class) +class CapabilitiesDslTest { + @Test + fun `capabilities should build minimal empty capabilities`() { + val request = buildInitializeRequest { + protocolVersion = "2024-11-05" + capabilities { } + info("Test", "1.0") + } + + request.params.capabilities.shouldNotBeNull { + sampling.shouldBeNull() + roots.shouldBeNull() + elicitation.shouldBeNull() + experimental.shouldBeNull() + extensions.shouldBeNull() + } + } + + @Test + fun `capabilities should build full with all fields and nested properties`() { + val request = buildInitializeRequest { + protocolVersion = "2024-11-05" + capabilities { + sampling(ClientCapabilities.Sampling(tools = EmptyJsonObject)) + roots(listChanged = true) + elicitation(ClientCapabilities.Elicitation(form = EmptyJsonObject, url = EmptyJsonObject)) + experimental { + put("customFeature", true) + put("version", "1.0") + put("beta", false) + put("maxConcurrency", 10) + } + extensions( + mapOf( + "io.modelcontextprotocol/ui" to buildJsonObject { + put("mimeTypes", "text/html") + }, + ), + ) + } + info("Test", "1.0") + } + + request.params.capabilities.shouldNotBeNull { + sampling shouldNotBeNull { + tools shouldBe EmptyJsonObject + } + roots shouldNotBeNull { + listChanged shouldBe true + } + elicitation shouldNotBeNull { + form shouldBe EmptyJsonObject + url shouldBe EmptyJsonObject + } + experimental shouldNotBeNull { + get("customFeature")?.jsonPrimitive?.content shouldBe "true" + get("version")?.jsonPrimitive?.content shouldBe "1.0" + get("beta")?.jsonPrimitive?.content shouldBe "false" + get("maxConcurrency")?.jsonPrimitive?.content shouldBe "10" + } + extensions shouldNotBeNull { + get("io.modelcontextprotocol/ui") shouldNotBeNull { + get("mimeTypes")?.jsonPrimitive?.content shouldBe "text/html" + } + } + } + } + + @Test + fun `capabilities should support roots variants`() { + // Test listChanged = true + val requestTrue = buildInitializeRequest { + protocolVersion = "2024-11-05" + capabilities { roots(listChanged = true) } + info("Test", "1.0") + } + requestTrue.params.capabilities.roots?.listChanged shouldBe true + + // Test listChanged = false + val requestFalse = buildInitializeRequest { + protocolVersion = "2024-11-05" + capabilities { roots(listChanged = false) } + info("Test", "1.0") + } + requestFalse.params.capabilities.roots?.listChanged shouldBe false + + // Test listChanged = null (not provided) + val requestNull = buildInitializeRequest { + protocolVersion = "2024-11-05" + capabilities { roots() } + info("Test", "1.0") + } + requestNull.params.capabilities.roots?.listChanged.shouldBeNull() + } + + @Test + fun `capabilities should build with extensions containing empty settings`() { + val request = buildInitializeRequest { + protocolVersion = "2024-11-05" + capabilities { + extensions( + mapOf( + "io.modelcontextprotocol/ui" to EmptyJsonObject, + "com.example/custom" to EmptyJsonObject, + ), + ) + } + info("Test", "1.0") + } + + request.params.capabilities.shouldNotBeNull { + extensions shouldNotBeNull { + size shouldBe 2 + get("io.modelcontextprotocol/ui") shouldBe EmptyJsonObject + get("com.example/custom") shouldBe EmptyJsonObject + } + } + } + + @Test + fun `capabilities should overwrite extensions when set multiple times`() { + val request = buildInitializeRequest { + protocolVersion = "2024-11-05" + capabilities { + extensions(mapOf("io.modelcontextprotocol/ui" to EmptyJsonObject)) + extensions(mapOf("com.example/custom" to EmptyJsonObject)) // Should overwrite + } + info("Test", "1.0") + } + + request.params.capabilities.shouldNotBeNull { + extensions shouldNotBeNull { + size shouldBe 1 + get("com.example/custom") shouldBe EmptyJsonObject + get("io.modelcontextprotocol/ui").shouldBeNull() + } + } + } + + @Test + fun `capabilities should overwrite when same field set multiple times`() { + val request = buildInitializeRequest { + protocolVersion = "2024-11-05" + capabilities { + sampling(ClientCapabilities.Sampling()) + sampling(ClientCapabilities.Sampling(tools = EmptyJsonObject)) // Should overwrite + experimental { put("feature", "v1") } + experimental { put("feature", "v2") } // Should overwrite + } + info("Test", "1.0") + } + + request.params.capabilities.shouldNotBeNull { + sampling shouldNotBeNull { + tools shouldBe EmptyJsonObject + } + experimental shouldNotBeNull { + get("feature")?.jsonPrimitive?.content shouldBe "v2" + } + } + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CompletionDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CompletionDslTest.kt new file mode 100644 index 0000000..cbe40c9 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CompletionDslTest.kt @@ -0,0 +1,65 @@ +package io.modelcontextprotocol.kotlin.sdk.types.dsl + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.PromptReference +import io.modelcontextprotocol.kotlin.sdk.types.ResourceTemplateReference +import io.modelcontextprotocol.kotlin.sdk.types.buildCompleteRequest +import kotlin.test.Test + +@OptIn(ExperimentalMcpApi::class) +class CompletionDslTest { + @Test + fun `buildCompleteRequest should build with prompt reference and context`() { + val request = buildCompleteRequest { + argument("query", "user input") + ref(PromptReference("searchPrompt")) + context { + put("userId", "123") + } + } + + request.params.argument.name shouldBe "query" + request.params.argument.value shouldBe "user input" + (request.params.ref as PromptReference).name shouldBe "searchPrompt" + request.params.context shouldNotBeNull { + arguments?.get("userId") shouldBe "123" + } + } + + @Test + fun `buildCompleteRequest should build with resource template reference and map context`() { + val request = buildCompleteRequest { + argument("path", "/users/123") + ref(ResourceTemplateReference("file:///{path}")) + context(mapOf("role" to "admin")) + } + + request.params.argument.name shouldBe "path" + request.params.argument.value shouldBe "/users/123" + (request.params.ref as ResourceTemplateReference).uri shouldBe "file:///{path}" + request.params.context shouldNotBeNull { + arguments?.get("role") shouldBe "admin" + } + } + + @Test + fun `buildCompleteRequest should throw if argument is missing`() { + shouldThrow { + buildCompleteRequest { + ref(PromptReference("name")) + } + } + } + + @Test + fun `buildCompleteRequest should throw if ref is missing`() { + shouldThrow { + buildCompleteRequest { + argument("name", "value") + } + } + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ContentDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ContentDslTest.kt new file mode 100644 index 0000000..4801d02 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ContentDslTest.kt @@ -0,0 +1,385 @@ +package io.modelcontextprotocol.kotlin.sdk.types.dsl + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.Annotations +import io.modelcontextprotocol.kotlin.sdk.types.AudioContent +import io.modelcontextprotocol.kotlin.sdk.types.ImageContent +import io.modelcontextprotocol.kotlin.sdk.types.Role +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.assistantAudio +import io.modelcontextprotocol.kotlin.sdk.types.assistantImage +import io.modelcontextprotocol.kotlin.sdk.types.buildCreateMessageRequest +import io.modelcontextprotocol.kotlin.sdk.types.userText +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.test.Test + +/** + * Tests for MediaContent DSL builders (TextContent, ImageContent, AudioContent). + * + * Verifies content can be constructed via DSL within sampling messages, + * covering minimal (required fields), full (all fields), validation, and edge cases. + */ +@OptIn(ExperimentalMcpApi::class) +class ContentDslTest { + + // ======================================================================== + // TextContent Tests + // ======================================================================== + + @Test + fun `userText should build minimal with text only`() { + val request = buildCreateMessageRequest { + maxTokens = 100 + messages { + userText { + text = "Hello, world!" + } + } + } + + (request.params.messages[0].content.single() as TextContent).shouldNotBeNull { + text shouldBe "Hello, world!" + annotations.shouldBeNull() + meta.shouldBeNull() + } + } + + @Test + fun `userText should build full with all fields and nested meta`() { + val request = buildCreateMessageRequest { + maxTokens = 100 + messages { + userText { + text = "Analyze the quarterly sales report for Q4 2025" + annotations( + audience = listOf(Role.User, Role.Assistant), + priority = 0.85, + lastModified = "2025-02-17T10:30:00Z", + ) + meta { + put("source", "dashboard") + put("userId", "user-123") + put("sessionId", 42) + put("analyzed", true) + } + } + } + } + + (request.params.messages[0].content.single() as TextContent).shouldNotBeNull { + text shouldBe "Analyze the quarterly sales report for Q4 2025" + annotations shouldNotBeNull { + audience shouldBe listOf(Role.User, Role.Assistant) + priority shouldBe 0.85 + lastModified shouldBe "2025-02-17T10:30:00Z" + } + meta shouldNotBeNull { + get("source")?.jsonPrimitive?.content shouldBe "dashboard" + get("userId")?.jsonPrimitive?.content shouldBe "user-123" + get("sessionId")?.jsonPrimitive?.content shouldBe "42" + get("analyzed")?.jsonPrimitive?.content shouldBe "true" + } + } + } + + @Test + fun `userText should handle unicode and special characters`() { + val request = buildCreateMessageRequest { + maxTokens = 100 + messages { + userText { + text = "Hello 🌍! Ça va? Москва 北京 مرحبا" + } + } + } + + (request.params.messages[0].content.single() as TextContent).text shouldBe "Hello 🌍! Ça va? Москва 北京 مرحبا" + } + + @Test + fun `userText should handle empty string`() { + val request = buildCreateMessageRequest { + maxTokens = 100 + messages { + userText { + text = "" + } + } + } + + (request.params.messages[0].content.single() as TextContent).text shouldBe "" + } + + @Test + fun `userText should throw if text is missing`() { + shouldThrow { + buildCreateMessageRequest { + maxTokens = 100 + messages { + userText { } + } + } + } + } + + // ======================================================================== + // ImageContent Tests + // ======================================================================== + + @Test + fun `assistantImage should build minimal with data and mimeType`() { + val request = buildCreateMessageRequest { + maxTokens = 100 + messages { + assistantImage { + data = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" + mimeType = "image/png" + } + } + } + + (request.params.messages[0].content.single() as ImageContent).shouldNotBeNull { + data shouldBe + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" + mimeType shouldBe "image/png" + annotations.shouldBeNull() + meta.shouldBeNull() + } + } + + @Test + fun `assistantImage should build full with all fields and image metadata`() { + val request = buildCreateMessageRequest { + maxTokens = 100 + messages { + assistantImage { + data = "iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFklEQVR42mNk+M9QzzCCgAEYjRZnRwEA" + mimeType = "image/jpeg" + annotations( + audience = listOf(Role.Assistant), + priority = 0.92, + lastModified = "2025-02-17T11:45:30Z", + ) + meta { + put("width", 1920) + put("height", 1080) + put("format", "jpeg") + put("compression", "lossy") + put("quality", 85) + } + } + } + } + + (request.params.messages[0].content.single() as ImageContent).shouldNotBeNull { + mimeType shouldBe "image/jpeg" + annotations shouldNotBeNull { + audience shouldBe listOf(Role.Assistant) + priority shouldBe 0.92 + lastModified shouldBe "2025-02-17T11:45:30Z" + } + meta shouldNotBeNull { + get("width")?.jsonPrimitive?.content shouldBe "1920" + get("height")?.jsonPrimitive?.content shouldBe "1080" + get("format")?.jsonPrimitive?.content shouldBe "jpeg" + get("compression")?.jsonPrimitive?.content shouldBe "lossy" + get("quality")?.jsonPrimitive?.content shouldBe "85" + } + } + } + + @Test + fun `assistantImage should accept various MIME types`() { + val mimeTypes = listOf("image/png", "image/jpeg", "image/webp", "image/gif", "image/svg+xml") + + mimeTypes.forEach { mime -> + val request = buildCreateMessageRequest { + maxTokens = 100 + messages { + assistantImage { + data = "base64data" + mimeType = mime + } + } + } + (request.params.messages[0].content.single() as ImageContent).mimeType shouldBe mime + } + } + + @Test + fun `assistantImage should throw if data is missing`() { + shouldThrow { + buildCreateMessageRequest { + maxTokens = 100 + messages { + assistantImage { + mimeType = "image/png" + } + } + } + } + } + + @Test + fun `assistantImage should throw if mimeType is missing`() { + shouldThrow { + buildCreateMessageRequest { + maxTokens = 100 + messages { + assistantImage { + data = "base64data" + } + } + } + } + } + + // ======================================================================== + // AudioContent Tests + // ======================================================================== + + @Test + fun `assistantAudio should build minimal with data and mimeType`() { + val request = buildCreateMessageRequest { + maxTokens = 100 + messages { + assistantAudio { + data = "UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAAABmYWN0BAAAAAAAAABkYXRhAAAAAA==" + mimeType = "audio/wav" + } + } + } + + (request.params.messages[0].content.single() as AudioContent).shouldNotBeNull { + data shouldBe "UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAAABmYWN0BAAAAAAAAABkYXRhAAAAAA==" + mimeType shouldBe "audio/wav" + annotations.shouldBeNull() + meta.shouldBeNull() + } + } + + @Test + fun `assistantAudio should build full with all fields and audio metadata`() { + val request = buildCreateMessageRequest { + maxTokens = 100 + messages { + assistantAudio { + data = "//uQxAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAADIADMzMzMzMzMzMzMzMzMzMzMzMzMzMzM" + mimeType = "audio/mpeg" + annotations( + audience = listOf(Role.User), + priority = 0.75, + lastModified = "2025-02-17T12:00:00Z", + ) + meta { + put("duration", 180) + put("bitrate", 128) + put("codec", "mp3") + put("sampleRate", 44100) + put("channels", 2) + } + } + } + } + + (request.params.messages[0].content.single() as AudioContent).shouldNotBeNull { + mimeType shouldBe "audio/mpeg" + annotations shouldNotBeNull { + audience shouldBe listOf(Role.User) + priority shouldBe 0.75 + lastModified shouldBe "2025-02-17T12:00:00Z" + } + meta shouldNotBeNull { + get("duration")?.jsonPrimitive?.content shouldBe "180" + get("bitrate")?.jsonPrimitive?.content shouldBe "128" + get("codec")?.jsonPrimitive?.content shouldBe "mp3" + get("sampleRate")?.jsonPrimitive?.content shouldBe "44100" + get("channels")?.jsonPrimitive?.content shouldBe "2" + } + } + } + + @Test + fun `assistantAudio should throw if data is missing`() { + shouldThrow { + buildCreateMessageRequest { + maxTokens = 100 + messages { + assistantAudio { + mimeType = "audio/wav" + } + } + } + } + } + + @Test + fun `assistantAudio should throw if mimeType is missing`() { + shouldThrow { + buildCreateMessageRequest { + maxTokens = 100 + messages { + assistantAudio { + data = "base64audio" + } + } + } + } + } + + // ======================================================================== + // Annotations Edge Cases + // ======================================================================== + + @Test + fun `annotations should support boundary priority values`() { + // Priority = 0.0 (minimum) + val requestMin = buildCreateMessageRequest { + maxTokens = 100 + messages { + userText { + text = "Min priority" + annotations(priority = 0.0) + } + } + } + (requestMin.params.messages[0].content.single() as TextContent).annotations?.priority shouldBe 0.0 + + // Priority = 1.0 (maximum) + val requestMax = buildCreateMessageRequest { + maxTokens = 100 + messages { + userText { + text = "Max priority" + annotations(priority = 1.0) + } + } + } + (requestMax.params.messages[0].content.single() as TextContent).annotations?.priority shouldBe 1.0 + } + + @Test + fun `annotations should support direct object construction`() { + val request = buildCreateMessageRequest { + maxTokens = 100 + messages { + userText { + text = "With annotations object" + annotations(Annotations(priority = 0.5, audience = listOf(Role.User))) + } + } + } + + (request.params.messages[0].content.single() as TextContent).annotations shouldNotBeNull { + priority shouldBe 0.5 + audience shouldBe listOf(Role.User) + lastModified.shouldBeNull() + } + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ElicitationDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ElicitationDslTest.kt new file mode 100644 index 0000000..4a34bab --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ElicitationDslTest.kt @@ -0,0 +1,89 @@ +package io.modelcontextprotocol.kotlin.sdk.types.dsl + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestFormParams +import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.StringSchema +import io.modelcontextprotocol.kotlin.sdk.types.buildElicitRequest +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlin.test.Test + +@OptIn(ExperimentalMcpApi::class) +class ElicitationDslTest { + @Test + fun `buildElicitRequest should build with all fields`() { + val request = buildElicitRequest { + message = "Provide info" + requestedSchema { + properties { + put("email", buildJsonObject { put("type", "string") }) + } + required = listOf("email") + } + } + + request.params.message shouldBe "Provide info" + val formParams = request.params.shouldBeInstanceOf() + formParams.requestedSchema.properties["email"].shouldBeInstanceOf() + formParams.requestedSchema.required shouldBe listOf("email") + } + + @Test + fun `buildElicitRequest should support direct requestedSchema`() { + val schema = ElicitRequestParams.RequestedSchema( + properties = mapOf("name" to StringSchema()), + required = listOf("name"), + ) + val request = buildElicitRequest { + message = "Test" + requestedSchema(schema) + } + + val formParams = request.params.shouldBeInstanceOf() + formParams.requestedSchema shouldBe schema + } + + @Test + fun `ElicitRequestedSchemaBuilder should support direct properties assignment`() { + val request = buildElicitRequest { + message = "Test" + requestedSchema { + properties(buildJsonObject { put("key", buildJsonObject { put("type", "string") }) }) + } + } + val formParams = request.params.shouldBeInstanceOf() + formParams.requestedSchema.properties["key"].shouldBeInstanceOf() + } + + @Test + fun `buildElicitRequest should throw if message is missing`() { + shouldThrow { + buildElicitRequest { + requestedSchema { properties { put("a", 1) } } + } + } + } + + @Test + fun `buildElicitRequest should throw if requestedSchema is missing`() { + shouldThrow { + buildElicitRequest { + message = "Test" + } + } + } + + @Test + fun `ElicitRequestedSchemaBuilder should throw if properties are missing`() { + shouldThrow { + buildElicitRequest { + message = "Test" + requestedSchema { } + } + } + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/InitializeDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/InitializeDslTest.kt new file mode 100644 index 0000000..da2a4cb --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/InitializeDslTest.kt @@ -0,0 +1,147 @@ +package io.modelcontextprotocol.kotlin.sdk.types.dsl + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.buildInitializeRequest +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.test.Test + +@OptIn(ExperimentalMcpApi::class) +class InitializeDslTest { + @Test + fun `buildInitializeRequest should create request with all fields`() { + val request = buildInitializeRequest { + protocolVersion = "2024-11-05" + capabilities { + sampling(ClientCapabilities.Sampling()) + roots(listChanged = true) + elicitation(ClientCapabilities.Elicitation(form = EmptyJsonObject)) + experimental { + put("custom", true) + } + extensions( + mapOf( + "io.modelcontextprotocol/ui" to EmptyJsonObject, + ), + ) + } + info( + name = "TestClient", + version = "1.0.0", + title = "Test Client", + websiteUrl = "https://example.com", + ) + } + + request.params.protocolVersion shouldBe "2024-11-05" + request.params.capabilities.shouldNotBeNull { + sampling shouldNotBeNull { } + roots?.listChanged shouldBe true + elicitation?.form shouldBe EmptyJsonObject + experimental?.get("custom")?.jsonPrimitive?.content shouldBe "true" + extensions shouldNotBeNull { + get("io.modelcontextprotocol/ui") shouldBe EmptyJsonObject + } + } + request.params.clientInfo.shouldNotBeNull { + name shouldBe "TestClient" + version shouldBe "1.0.0" + title shouldBe "Test Client" + websiteUrl shouldBe "https://example.com" + } + } + + @Test + fun `buildInitializeRequest should support direct capabilities and info`() { + val capabilities = ClientCapabilities(roots = ClientCapabilities.Roots(listChanged = false)) + val info = Implementation(name = "Direct", version = "0.1") + + val request = buildInitializeRequest { + protocolVersion = "1.0" + capabilities(capabilities) + info(info) + } + + request.params.capabilities shouldBe capabilities + request.params.clientInfo shouldBe info + } + + @Test + fun `capabilities DSL should support direct typed values`() { + val samplingValue = ClientCapabilities.Sampling(tools = EmptyJsonObject) + val elicitationValue = ClientCapabilities.Elicitation(form = EmptyJsonObject, url = EmptyJsonObject) + val experimentalObj = buildJsonObject { put("key", "value") } + val extensionsMap = mapOf( + "io.modelcontextprotocol/ui" to buildJsonObject { put("key", "value") }, + ) + + val request = buildInitializeRequest { + protocolVersion = "1.0" + capabilities { + sampling(samplingValue) + elicitation(elicitationValue) + experimental(experimentalObj) + extensions(extensionsMap) + } + info("Test", "1.0") + } + + request.params.capabilities.shouldNotBeNull { + sampling shouldBe samplingValue + elicitation shouldBe elicitationValue + experimental shouldBe experimentalObj + extensions shouldBe extensionsMap + } + } + + @Test + fun `ClientCapabilitiesBuilder roots should support default arguments`() { + val request = buildInitializeRequest { + protocolVersion = "1.0" + capabilities { + roots() + } + info("Test", "1.0") + } + request.params.capabilities.roots.shouldNotBeNull { + listChanged shouldBe null + } + } + + @Test + fun `buildInitializeRequest should throw if protocolVersion is missing`() { + shouldThrow { + buildInitializeRequest { + capabilities { } + info("Test", "1.0") + } + } + } + + @Test + fun `buildInitializeRequest should throw if capabilities are missing`() { + shouldThrow { + buildInitializeRequest { + protocolVersion = "1.0" + info("Test", "1.0") + } + } + } + + @Test + fun `buildInitializeRequest should throw if info is missing`() { + shouldThrow { + buildInitializeRequest { + protocolVersion = "1.0" + capabilities { } + } + } + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/LoggingDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/LoggingDslTest.kt new file mode 100644 index 0000000..5b09b42 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/LoggingDslTest.kt @@ -0,0 +1,27 @@ +package io.modelcontextprotocol.kotlin.sdk.types.dsl + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel +import io.modelcontextprotocol.kotlin.sdk.types.buildSetLevelRequest +import kotlin.test.Test + +@OptIn(ExperimentalMcpApi::class) +class LoggingDslTest { + @Test + fun `buildSetLevelRequest should create request with given level`() { + val request = buildSetLevelRequest { + loggingLevel = LoggingLevel.Info + } + + request.params.level shouldBe LoggingLevel.Info + } + + @Test + fun `buildSetLevelRequest should throw if loggingLevel is missing`() { + shouldThrow { + buildSetLevelRequest { } + } + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/PingRequestDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/PingRequestDslTest.kt new file mode 100644 index 0000000..79cef06 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/PingRequestDslTest.kt @@ -0,0 +1,69 @@ +package io.modelcontextprotocol.kotlin.sdk.types.dsl + +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.buildPingRequest +import kotlinx.serialization.json.add +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.test.Test + +@OptIn(ExperimentalMcpApi::class) +class PingRequestDslTest { + @Test + fun `buildPingRequest should create request with meta containing all field types`() { + val request = buildPingRequest { + meta { + progressToken("token-123") + put("string", "value") + put("number", 42) + put("boolean", true) + put("null", null) + putJsonObject("obj") { + put("key", "val") + } + putJsonArray("arr") { + add("item") + } + } + } + + request.params.shouldNotBeNull { + meta.shouldNotBeNull { + json["progressToken"]?.jsonPrimitive?.content shouldBe "token-123" + json["string"]?.jsonPrimitive?.content shouldBe "value" + json["number"]?.jsonPrimitive?.int shouldBe 42 + json["boolean"]?.jsonPrimitive?.boolean shouldBe true + json["null"] shouldBe kotlinx.serialization.json.JsonNull + json["obj"]?.shouldNotBeNull() + json["arr"]?.shouldNotBeNull() + } + } + } + + @Test + fun `RequestMeta DSL should support numeric progress tokens`() { + val requestInt = buildPingRequest { + meta { progressToken(123) } + } + requestInt.params?.meta?.json + ?.get("progressToken") + ?.jsonPrimitive?.int shouldBe 123 + + val requestLong = buildPingRequest { + meta { progressToken(456L) } + } + requestLong.params?.meta?.json + ?.get("progressToken") + ?.jsonPrimitive?.int shouldBe 456 + } + + @Test + fun `buildPingRequest should create request without params if meta is empty`() { + val request = buildPingRequest { } + request.params shouldBe null + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/PromptsDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/PromptsDslTest.kt new file mode 100644 index 0000000..937933e --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/PromptsDslTest.kt @@ -0,0 +1,47 @@ +package io.modelcontextprotocol.kotlin.sdk.types.dsl + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.buildGetPromptRequest +import io.modelcontextprotocol.kotlin.sdk.types.buildListPromptsRequest +import kotlin.test.Test + +@OptIn(ExperimentalMcpApi::class) +class PromptsDslTest { + @Test + fun `buildGetPromptRequest should create request with name and arguments`() { + val request = buildGetPromptRequest { + name = "test-prompt" + arguments = mapOf("key" to "value") + } + + request.params.name shouldBe "test-prompt" + request.params.arguments shouldBe mapOf("key" to "value") + } + + @Test + fun `buildListPromptsRequest should create request with cursor`() { + val request = buildListPromptsRequest { + cursor = "next-page" + } + + request.params shouldNotBeNull { + cursor shouldBe "next-page" + } + } + + @Test + fun `buildGetPromptRequest should throw if name is missing`() { + shouldThrow { + buildGetPromptRequest { } + } + } + + @Test + fun `buildListPromptsRequest should create request without params if empty`() { + val request = buildListPromptsRequest { } + request.params shouldBe null + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/RequestDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/RequestDslTest.kt new file mode 100644 index 0000000..ad9c719 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/RequestDslTest.kt @@ -0,0 +1,256 @@ +package io.modelcontextprotocol.kotlin.sdk.types.dsl + +import io.kotest.matchers.collections.shouldContainAll +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.RequestId +import io.modelcontextprotocol.kotlin.sdk.types.buildListToolsRequest +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.add +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long +import kotlinx.serialization.json.put +import kotlin.test.Test + +/** + * Tests for RequestMeta and PaginatedRequest DSL builders. + * + * Verifies meta and cursor can be constructed via DSL within paginated requests, + * covering minimal (no params), full (all field types), variants, and edge cases. + */ +@OptIn(ExperimentalMcpApi::class) +class RequestDslTest { + @Test + fun `request should build minimal without any params`() { + val request = buildListToolsRequest { } + + request.params.shouldBeNull() + request.method shouldBe Method.Defined.ToolsList + } + + @Test + fun `request should build full with cursor and meta containing all field types`() { + val request = buildListToolsRequest { + cursor = "next-page-eyJvZmZzZXQiOjEwMH0" + meta { + // ProgressToken + progressToken("progress-abc123") + + // String fields + put("requestId", "req-456") + put("source", "api-gateway") + + // Number fields + put("priority", 5) + put("retryCount", 3L) + put("timeout", 30000) + + // Boolean fields + put("urgent", true) + put("cached", false) + + // Null field + put("optional", null) + + // Nested JsonObject + putJsonObject("context") { + put("userId", "user-789") + put("sessionId", "sess-abc") + put("authenticated", true) + put("permissions", 7) + } + + // JsonArray + putJsonArray("tags") { + add("production") + add("critical") + add("monitored") + } + } + } + + request.params shouldNotBeNull { + cursor shouldBe "next-page-eyJvZmZzZXQiOjEwMH0" + meta shouldNotBeNull { + // ProgressToken + progressToken shouldNotBeNull { + shouldBeInstanceOf() + value shouldBe "progress-abc123" + } + + // String fields + get("requestId")?.jsonPrimitive?.content shouldBe "req-456" + get("source")?.jsonPrimitive?.content shouldBe "api-gateway" + + // Number fields + get("priority")?.jsonPrimitive?.int shouldBe 5 + get("retryCount")?.jsonPrimitive?.long shouldBe 3L + get("timeout")?.jsonPrimitive?.int shouldBe 30000 + + // Boolean fields + get("urgent")?.jsonPrimitive?.boolean shouldBe true + get("cached")?.jsonPrimitive?.boolean shouldBe false + + // Null field + get("optional") shouldBe JsonNull + + // Nested JsonObject + get("context") shouldNotBeNull { + jsonObject["userId"]?.jsonPrimitive?.content shouldBe "user-789" + jsonObject["sessionId"]?.jsonPrimitive?.content shouldBe "sess-abc" + jsonObject["authenticated"]?.jsonPrimitive?.boolean shouldBe true + jsonObject["permissions"]?.jsonPrimitive?.int shouldBe 7 + } + + // JsonArray + get("tags") shouldNotBeNull { + jsonArray.map { it.jsonPrimitive.content } shouldContainAll listOf( + "production", + "critical", + "monitored", + ) + } + } + } + } + + @Test + fun `meta progressToken should support String Int and Long types`() { + // String progressToken + val stringRequest = buildListToolsRequest { + meta { progressToken("token-abc") } + } + stringRequest.params?.meta?.progressToken shouldNotBeNull { + shouldBeInstanceOf() + value shouldBe "token-abc" + } + + // Int progressToken + val intRequest = buildListToolsRequest { + meta { progressToken(42) } + } + intRequest.params?.meta?.progressToken shouldNotBeNull { + shouldBeInstanceOf() + value shouldBe 42L + } + + // Long progressToken + val longRequest = buildListToolsRequest { + meta { progressToken(999L) } + } + longRequest.params?.meta?.progressToken shouldNotBeNull { + shouldBeInstanceOf() + value shouldBe 999L + } + } + + @Test + fun `meta should support custom fields without progressToken`() { + val request = buildListToolsRequest { + meta { + put("requestId", "req-123") + put("source", "cli") + // No progressToken + } + } + + request.params shouldNotBeNull { + meta shouldNotBeNull { + progressToken.shouldBeNull() + get("requestId")?.jsonPrimitive?.content shouldBe "req-123" + get("source")?.jsonPrimitive?.content shouldBe "cli" + } + } + } + + @Test + fun `cursor should work without meta`() { + val request = buildListToolsRequest { + cursor = "page-2-cursor" + } + + request.params shouldNotBeNull { + cursor shouldBe "page-2-cursor" + meta.shouldBeNull() + } + } + + @Test + fun `cursor should handle empty string`() { + val request = buildListToolsRequest { + cursor = "" + } + + request.params shouldNotBeNull { + cursor shouldBe "" + } + } + + @Test + fun `meta should handle special characters in keys`() { + val request = buildListToolsRequest { + meta { + put("key-with-dashes", "value1") + put("key.with.dots", "value2") + put("key_with_underscores", "value3") + put("keyWithCamelCase", "value4") + } + } + + request.params?.meta shouldNotBeNull { + get("key-with-dashes")?.jsonPrimitive?.content shouldBe "value1" + get("key.with.dots")?.jsonPrimitive?.content shouldBe "value2" + get("key_with_underscores")?.jsonPrimitive?.content shouldBe "value3" + get("keyWithCamelCase")?.jsonPrimitive?.content shouldBe "value4" + } + } + + @Test + fun `meta should overwrite when progressToken set multiple times`() { + val request = buildListToolsRequest { + meta { + progressToken("first") + progressToken(123) // Different type + progressToken("second") // Back to string, should be final + } + } + + request.params?.meta?.progressToken shouldNotBeNull { + shouldBeInstanceOf() + value shouldBe "second" + } + } + + @Test + fun `meta should overwrite when custom field set multiple times`() { + val request = buildListToolsRequest { + meta { + put("key", "first") + put("key", 123) // Different type + put("key", "third") // Back to string, should be final + } + } + + request.params?.meta shouldNotBeNull { + get("key")?.jsonPrimitive?.content shouldBe "third" + } + } + + @Test + fun `meta should handle very long cursor strings`() { + val longCursor = "x".repeat(1000) + val request = buildListToolsRequest { + cursor = longCursor + } + + request.params?.cursor shouldBe longCursor + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ResourcesDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ResourcesDslTest.kt new file mode 100644 index 0000000..13df670 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ResourcesDslTest.kt @@ -0,0 +1,92 @@ +package io.modelcontextprotocol.kotlin.sdk.types.dsl + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.buildListResourceTemplatesRequest +import io.modelcontextprotocol.kotlin.sdk.types.buildListResourcesRequest +import io.modelcontextprotocol.kotlin.sdk.types.buildReadResourceRequest +import io.modelcontextprotocol.kotlin.sdk.types.buildSubscribeRequest +import io.modelcontextprotocol.kotlin.sdk.types.buildUnsubscribeRequest +import kotlin.test.Test + +@OptIn(ExperimentalMcpApi::class) +class ResourcesDslTest { + @Test + fun `buildListResourcesRequest should create request with cursor`() { + val request = buildListResourcesRequest { + cursor = "next" + } + request.params shouldNotBeNull { + cursor shouldBe "next" + } + } + + @Test + fun `buildReadResourceRequest should create request with uri`() { + val request = buildReadResourceRequest { + uri = "test://resource" + } + request.params.uri shouldBe "test://resource" + } + + @Test + fun `buildSubscribeRequest should create request with uri`() { + val request = buildSubscribeRequest { + uri = "test://resource" + } + request.params.uri shouldBe "test://resource" + } + + @Test + fun `buildUnsubscribeRequest should create request with uri`() { + val request = buildUnsubscribeRequest { + uri = "test://resource" + } + request.params.uri shouldBe "test://resource" + } + + @Test + fun `buildListResourceTemplatesRequest should create request with cursor`() { + val request = buildListResourceTemplatesRequest { + cursor = "template-cursor" + } + request.params shouldNotBeNull { + cursor shouldBe "template-cursor" + } + } + + @Test + fun `buildReadResourceRequest should throw if uri is missing`() { + shouldThrow { + buildReadResourceRequest { } + } + } + + @Test + fun `buildSubscribeRequest should throw if uri is missing`() { + shouldThrow { + buildSubscribeRequest { } + } + } + + @Test + fun `buildUnsubscribeRequest should throw if uri is missing`() { + shouldThrow { + buildUnsubscribeRequest { } + } + } + + @Test + fun `buildListResourcesRequest should create request without params if empty`() { + val request = buildListResourcesRequest { } + request.params shouldBe null + } + + @Test + fun `buildListResourceTemplatesRequest should create request without params if empty`() { + val request = buildListResourceTemplatesRequest { } + request.params shouldBe null + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/RootsDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/RootsDslTest.kt new file mode 100644 index 0000000..037649f --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/RootsDslTest.kt @@ -0,0 +1,19 @@ +package io.modelcontextprotocol.kotlin.sdk.types.dsl + +import io.kotest.matchers.nulls.shouldNotBeNull +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.buildListRootsRequest +import kotlin.test.Test + +@OptIn(ExperimentalMcpApi::class) +class RootsDslTest { + @Test + fun `buildListRootsRequest should create request with meta`() { + val request = buildListRootsRequest { + meta { + put("test", "value") + } + } + request.params.shouldNotBeNull() + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/SamplingDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/SamplingDslTest.kt new file mode 100644 index 0000000..98817c1 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/SamplingDslTest.kt @@ -0,0 +1,201 @@ +package io.modelcontextprotocol.kotlin.sdk.types.dsl + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.Annotations +import io.modelcontextprotocol.kotlin.sdk.types.AudioContent +import io.modelcontextprotocol.kotlin.sdk.types.ImageContent +import io.modelcontextprotocol.kotlin.sdk.types.IncludeContext +import io.modelcontextprotocol.kotlin.sdk.types.ModelPreferences +import io.modelcontextprotocol.kotlin.sdk.types.Role +import io.modelcontextprotocol.kotlin.sdk.types.SamplingMessage +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.assistant +import io.modelcontextprotocol.kotlin.sdk.types.assistantAudio +import io.modelcontextprotocol.kotlin.sdk.types.assistantImage +import io.modelcontextprotocol.kotlin.sdk.types.buildCreateMessageRequest +import io.modelcontextprotocol.kotlin.sdk.types.user +import io.modelcontextprotocol.kotlin.sdk.types.userAudio +import io.modelcontextprotocol.kotlin.sdk.types.userImage +import io.modelcontextprotocol.kotlin.sdk.types.userText +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.test.Test + +@OptIn(ExperimentalMcpApi::class) +class SamplingDslTest { + @Test + fun `buildCreateMessageRequest should build with all fields`() { + val request = buildCreateMessageRequest { + maxTokens = 1000 + systemPrompt = "System prompt" + temperature = 0.5 + stopSequences = listOf("STOP") + messages { + user { "Hello" } + assistant { "Hi" } + userText { + text = "Text with annotations" + annotations( + audience = listOf(Role.User), + priority = 1.0, + lastModified = "2025-01-10T00:00:00Z", + ) + meta { + put("key", "value") + } + } + assistantImage { + data = "base64image" + mimeType = "image/png" + annotations(Annotations(priority = 0.5)) + } + assistantAudio { + data = "base64audio" + mimeType = "audio/wav" + } + } + preferences(hints = listOf("hint"), intelligence = 0.9) + metadata { + put("metaKey", "metaValue") + } + } + + request.params.maxTokens shouldBe 1000 + request.params.systemPrompt shouldBe "System prompt" + request.params.temperature shouldBe 0.5 + request.params.stopSequences shouldBe listOf("STOP") + request.params.messages shouldHaveSize 5 + + (request.params.messages[2].content.single() as TextContent).shouldNotBeNull { + text shouldBe "Text with annotations" + annotations shouldNotBeNull { + audience shouldBe listOf(Role.User) + priority shouldBe 1.0 + lastModified shouldBe "2025-01-10T00:00:00Z" + } + meta?.get("key")?.jsonPrimitive?.content shouldBe "value" + } + + (request.params.messages[3].content.single() as ImageContent).shouldNotBeNull { + data shouldBe "base64image" + mimeType shouldBe "image/png" + annotations?.priority shouldBe 0.5 + } + + (request.params.messages[4].content.single() as AudioContent).shouldNotBeNull { + data shouldBe "base64audio" + mimeType shouldBe "audio/wav" + } + + request.params.modelPreferences shouldNotBeNull { + intelligencePriority shouldBe 0.9 + } + request.params.metadata shouldNotBeNull { + get("metaKey")?.jsonPrimitive?.content shouldBe "metaValue" + } + } + + @Test + fun `buildCreateMessageRequest should support direct assignments`() { + val messages = listOf(SamplingMessage(Role.User, listOf(TextContent("Hello")))) + val preferences = ModelPreferences(costPriority = 0.1) + val request = buildCreateMessageRequest { + maxTokens = 100 + messages(messages) + preferences(preferences) + context = IncludeContext.AllServers + } + + request.params.messages shouldBe messages + request.params.modelPreferences shouldBe preferences + request.params.includeContext shouldBe IncludeContext.AllServers + } + + @Test + fun `SamplingMessageBuilder should support direct content assignment`() { + val content = TextContent("Direct") + val request = buildCreateMessageRequest { + maxTokens = 100 + messages { + user(content) + assistant(content) + } + } + request.params.messages[0].content.single() shouldBe content + request.params.messages[1].content.single() shouldBe content + } + + @Test + fun `buildCreateMessageRequest should throw if maxTokens is missing`() { + shouldThrow { + buildCreateMessageRequest { + messages { user { "Hi" } } + } + } + } + + @Test + fun `buildCreateMessageRequest should throw if messages are missing`() { + shouldThrow { + buildCreateMessageRequest { + maxTokens = 100 + } + } + } + + @Test + fun `TextContentBuilder should throw if text is missing`() { + shouldThrow { + buildCreateMessageRequest { + maxTokens = 100 + messages { + userText { } + } + } + } + } + + @Test + fun `ImageContentBuilder should throw if data or mimeType is missing`() { + shouldThrow { + buildCreateMessageRequest { + maxTokens = 100 + messages { + userImage { data = "abc" } + } + } + } + shouldThrow { + buildCreateMessageRequest { + maxTokens = 100 + messages { + userImage { mimeType = "image/png" } + } + } + } + } + + @Test + fun `AudioContentBuilder should throw if data or mimeType is missing`() { + shouldThrow { + buildCreateMessageRequest { + maxTokens = 100 + messages { + userAudio { data = "abc" } + } + } + } + shouldThrow { + buildCreateMessageRequest { + maxTokens = 100 + messages { + userAudio { mimeType = "audio/wav" } + } + } + } + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ToolsDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ToolsDslTest.kt new file mode 100644 index 0000000..957f0d2 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ToolsDslTest.kt @@ -0,0 +1,67 @@ +package io.modelcontextprotocol.kotlin.sdk.types.dsl + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.buildCallToolRequest +import io.modelcontextprotocol.kotlin.sdk.types.buildListToolsRequest +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.test.Test + +@OptIn(ExperimentalMcpApi::class) +class ToolsDslTest { + @Test + fun `buildCallToolRequest should build with name and arguments`() { + val request = buildCallToolRequest { + name = "test-tool" + arguments { + put("key", "value") + put("count", 1) + } + } + + request.params.name shouldBe "test-tool" + request.params.arguments shouldNotBeNull { + get("key")?.jsonPrimitive?.content shouldBe "value" + get("count")?.jsonPrimitive?.int shouldBe 1 + } + } + + @Test + fun `buildListToolsRequest should build with cursor`() { + val request = buildListToolsRequest { + cursor = "tool-cursor" + } + + request.params shouldNotBeNull { + cursor shouldBe "tool-cursor" + } + } + + @Test + fun `buildCallToolRequest should support direct arguments assignment`() { + val args = buildJsonObject { put("key", "value") } + val request = buildCallToolRequest { + name = "test-tool" + arguments(args) + } + request.params.arguments shouldBe args + } + + @Test + fun `buildCallToolRequest should throw if name is missing`() { + shouldThrow { + buildCallToolRequest { } + } + } + + @Test + fun `buildListToolsRequest should build without params if empty`() { + val request = buildListToolsRequest { } + request.params shouldBe null + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcherSecurityTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcherSecurityTest.kt new file mode 100644 index 0000000..fe3fca4 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcherSecurityTest.kt @@ -0,0 +1,221 @@ +package io.modelcontextprotocol.kotlin.sdk.utils + +import io.kotest.assertions.withClue +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.modelcontextprotocol.kotlin.sdk.types.ResourceTemplate +import kotlin.test.Test + +/** + * Security regression tests for [PathSegmentTemplateMatcher]. + * + * Every test **fails** when an attack succeeds — green = defended. + * + * Findings addressed: + * - F1: Exactly one percent-decode pass (no double-decoding). + * - F2: Variable values are attacker-controlled; handlers must validate them. + * - F3: URI segment depth cap prevents allocation amplification. + * - F4: Dot-segment normalization blocks traversal on JVM/JS (no-op on native/WASM). + * - F5: Double-slash — collapsed by JVM normalization or causes size mismatch on native/WASM. + */ +class PathSegmentTemplateMatcherSecurityTest { + + // region: F4: Dot-segment traversal + + /** + * URIs containing `..` or `.` path segments. + * + * On JVM/JS: [normalizeUri] resolves these to their canonical target path, so the + * traversal URI correctly routes to the same template the normalized path would match. + * On native/WASM: [normalizeUri] is a no-op — the extra segments produce a size mismatch + * and the match returns null (different reason, same safety outcome). + */ + private data class TraversalCase(val label: String, val template: String, val uri: String) + + private val traversalNoMatchCases = listOf( + TraversalCase( + label = "single .. traversal to reach private template", + template = "app://host/private/{name}", + uri = "app://host/public/../private/secret", + ), + TraversalCase( + label = "double .. traversal", + template = "app://host/admin/{name}", + uri = "app://host/a/b/../../admin/config", + ), + TraversalCase( + label = ". current-segment in path", + template = "app://host/users/{id}", + uri = "app://host/./users/42", + ), + ) + + @Test + fun `dot-segment traversal resolves to correct target without raw path markers in variables`() { + // On JVM/JS: normalizeUri() resolves `..` and `.` before segment matching. + // → The traversal URI correctly routes to the normalized target template. + // On native/WASM: no normalization — extra segments cause a size mismatch → null. + // In all cases, captured variable values must not contain raw traversal markers. + for ((label, template, uri) in traversalNoMatchCases) { + withClue("traversal '$label': uri='$uri', template='$template'") { + val result = matcher(template).match(uri) + result?.variables?.values?.forEach { value -> + withClue("variable value '$value' must not contain raw '..' or '.' traversal marker") { + value.contains("..") shouldBe false + value shouldNotBe "." + } + } + } + } + } + + @Test + fun `dot-segment URI matches the template it normalizes to`() { + // "public/../users/42" normalizes to "users/42" on JVM/JS, no-op on native/WASM. + // On JVM/JS this must match; the test is platform-conditional via expect/actual. + val result = matcher("users/{id}").match("users/./42") + // After normalization "users/./42" → "users/42" (JVM/JS) or stays 3 segments (native/WASM). + // Either null (native/WASM — 3 segments vs 2) or a valid match (JVM/JS) is acceptable. + // What must NOT happen is a wrong-template match. We check the value if it matches. + result?.variables?.get("id") shouldBe result?.let { "42" } + } + + // endregion + // region: F1: Single percent-decode pass (no double-decoding) + + /** + * %25 is the encoding of '%'. %252F = "%2F" after one decode pass — must NOT become "/". + * If two decode passes were applied, %252F → %2F → /, enabling path traversal. + */ + @Test + fun `double-encoded percent-sign is decoded only once`() { + val result = matcher("files/{path}").match("files/%252Fetc%252Fpasswd") + result.shouldNotBeNull { + // One decode pass: %25 → %, %2F → /... but only %25 is decoded first. + // %252F → %2F (the %25 decodes to %, leaving literal "%2F") + withClue("double-encoded %2F must not become a slash after one decode pass") { + variables["path"]?.contains('/') shouldBe false + } + } + } + + @Test + fun `percent-encoded slash percent-2F in variable is decoded to slash`() { + // %2F IS decoded (one pass) — the result is "/" in the variable value. + // This documents the known behavior: handlers receive decoded values and must + // validate that path separators are not present when constructing file paths. + val result = matcher("files/{path}").match("files/..%2Fetc%2Fpasswd") + result.shouldNotBeNull { + withClue("decoded %2F becomes a path separator — handler must validate") { + variables["path"] shouldBe "../etc/passwd" + } + } + } + + @Test + fun `null byte percent-00 is decoded and passed to handler`() { + // %00 decodes to the null character. Handlers using C-string APIs or file paths + // must reject values containing null bytes. + val result = matcher("items/{id}").match("items/foo%00bar") + result.shouldNotBeNull { + withClue("decoded null byte is present — handler must reject") { + variables["id"] shouldBe "foo\u0000bar" + } + } + } + + @Test + fun `pct-encoded identity bypass`() { + // %69 = 'i'. "adm%69n" decodes to "admin". If a handler compares the raw URI + // against a deny-list of "admin", it must use the decoded value. + val result = matcher("svc/{role}").match("svc/adm%69n") + result.shouldNotBeNull { + variables["role"] shouldBe "admin" + } + } + + // endregion + // region: F3: URI segment depth cap + + @Test + fun `URI exceeding MAX_DEPTH segments returns null`() { + // 51 slash-separated single-char segments → well over the depth cap of 50 + val deepUri = (1..51).joinToString("/") { "x" } + matcher("{a}").match(deepUri) shouldBe null + } + + @Test + fun `URI at exactly MAX_DEPTH segments is not rejected by depth cap`() { + // A URI with exactly 50 segments must be evaluated normally (still non-matching + // against a 1-segment template, but not rejected by the depth guard itself). + val atLimit = (1..50).joinToString("/") { "x" } + // Template has 1 segment, URI has 50 — size mismatch, not depth rejection. + // Both code paths return null; this test verifies no exception is thrown. + matcher("{id}").match(atLimit) shouldBe null + } + + @Test + fun `many-segment URI against many templates completes quickly`() { + val deepUri = (1..49).joinToString("/") { "seg$it" } + val matchers = (1..100).map { matcher("{v$it}") } + // Must not allocate or compute excessively — just verify it finishes. + matchers.forEach { it.match(deepUri) } + } + + // endregion + // region: F5: Double-slash does not produce spurious matches + + @Test + fun `double-slash in URI path is normalized on JVM or causes size mismatch elsewhere`() { + // On JVM: URI("users//42").normalize() collapses "//" to "/" → "users/42" → matches. + // On native/WASM: no normalization — split produces 3 segments vs 2 in template → null. + // Either outcome is safe: no empty-segment injection reaches the handler unchecked. + val result = matcher("users/{id}").match("users//42") + // If matched (JVM), the captured value must be the legitimate path segment. + result?.variables?.get("id") shouldBe result?.let { "42" } + } + + @Test + fun `double-slash in scheme authority path is normalized on JVM or causes size mismatch elsewhere`() { + // On JVM: URI("app://host//injected").normalize() collapses the path "//" to "/" + // → "app://host/injected" → matches "app://host/{id}". + // On native/WASM: no normalization — extra empty segment causes size mismatch → null. + val result = matcher("app://host/{id}").match("app://host//injected") + // If matched (JVM), the captured value must be the legitimate path segment. + result?.variables?.get("id") shouldBe result?.let { "injected" } + } + + // endregion + // region: Query string and fragment pass-through + + @Test + fun `query string and fragment are captured in variable values - handlers must validate`() { + // PathSegmentTemplateMatcher splits on '/' only and does not strip query + // strings or fragments. They are captured verbatim inside the variable value. + // Handlers MUST reject or strip '?' and '#' before using values in file paths, + // database queries, or any other security-sensitive operation. + + // Segment split of "api://host/foo?bar=baz" → ["api:", "", "host", "foo?bar=baz"] (4). + // Template "api://host/{id}" → ["api:", "", "host", "{id}"] (4). + // → Matches on all platforms; query string ends up in the variable value. + val queryResult = matcher("api://host/{id}").match("api://host/foo?bar=baz") + queryResult.shouldNotBeNull { + variables["id"] shouldBe "foo?bar=baz" + } + + // Fragment is also captured verbatim — same reasoning. + val fragmentResult = matcher("api://host/{id}").match("api://host/foo#section") + fragmentResult.shouldNotBeNull { + variables["id"] shouldBe "foo#section" + } + } + + // endregion + // region: Helpers + + private fun matcher(uriTemplate: String): PathSegmentTemplateMatcher = PathSegmentTemplateMatcher( + resourceTemplate = ResourceTemplate(uriTemplate, "Test"), + ) + // endregion +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcherTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcherTest.kt new file mode 100644 index 0000000..2f43b5e --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcherTest.kt @@ -0,0 +1,236 @@ +package io.modelcontextprotocol.kotlin.sdk.utils + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.maps.shouldBeEmpty +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.types.ResourceTemplate +import kotlin.test.Test + +class PathSegmentTemplateMatcherTest { + + // region: Construction + + @Test + fun `should throw on blank template`() { + shouldThrow { + matcher(" ") + } + } + + @Test + fun `should throw on empty variable name`() { + shouldThrow { + matcher("users/{}") + } + } + + @Test + fun `should throw when depth exceeds maxTemplateDepth`() { + val deep = "a/b/c/d/e/f/g/h/i/j/k" // 11 segments + shouldThrow { + matcher(deep, maxDepth = 10) + } + } + + @Test + fun `should accept template at exactly maxTemplateDepth`() { + val atLimit = "a/b/c/d/e/f/g/h/i/j" // 10 segments + matcher(atLimit, maxDepth = 10) // must not throw + } + + // endregion + // region: Basic matching + + @Test + fun `should return null for URI with fewer segments than template`() { + matcher("users/{id}/posts").match("users/42") shouldBe null + } + + @Test + fun `should return null for URI with more segments than template`() { + matcher("users/{id}").match("users/42/extra") shouldBe null + } + + @Test + fun `should match all-literal template`() { + val result = matcher("users/profile").match("users/profile") + result.shouldNotBeNull { + variables.shouldBeEmpty() + } + } + + @Test + fun `should return null when literal segment does not match`() { + matcher("users/profile").match("users/settings") shouldBe null + } + + @Test + fun `should extract single variable`() { + val result = matcher("users/{id}").match("users/42") + result.shouldNotBeNull { + variables["id"] shouldBe "42" + } + } + + @Test + fun `should extract multiple variables`() { + val result = matcher("users/{userId}/posts/{postId}").match("users/alice/posts/99") + result.shouldNotBeNull { + variables["userId"] shouldBe "alice" + variables["postId"] shouldBe "99" + } + } + + @Test + fun `should match template with scheme`() { + val result = matcher("test://items/{id}").match("test://items/42") + result.shouldNotBeNull { + variables["id"] shouldBe "42" + } + } + + // endregion + // region:Scoring + + @Test + fun `all-literal template scores higher than parameterized template for same URI`() { + val literal = matcher("users/profile").match("users/profile")!! + val parameterized = matcher("users/{id}").match("users/profile")!! + (literal.score > parameterized.score) shouldBe true + } + + @Test + fun `score increases with number of segments`() { + val short = matcher("a/b").match("a/b")!! + val long = matcher("a/b/c").match("a/b/c")!! + (long.score > short.score) shouldBe true + } + + @Test + fun `all-literal two-segment template score is 4`() { + // 2 literal segments × LITERAL_MATCH_SCORE(2) = 4 + matcher("users/profile").match("users/profile")!!.score shouldBe 4 + } + + @Test + fun `one-variable two-segment template score is 3`() { + // 1 literal × 2 + 1 variable × 1 = 3 + matcher("users/{id}").match("users/42")!!.score shouldBe 3 + } + + @Test + fun `all-variable template scores one per segment`() { + // 2 variables × VARIABLE_MATCH_SCORE(1) = 2 + matcher("{a}/{b}").match("x/y")!!.score shouldBe 2 + } + + // endregion + // region:URL decoding + + @Test + fun `should URL-decode percent-encoded variable value`() { + val result = matcher("search/{query}").match("search/hello%20world") + result.shouldNotBeNull { + variables["query"] shouldBe "hello world" + } + } + + @Test + fun `should URL-decode percent-encoded literal segment before comparing`() { + // %66 decodes to 'f', so "pro%66ile" == "profile" after decoding — it matches + matcher("users/profile").match("users/pro%66ile").shouldNotBeNull() + } + + // endregion + // region:Length guard + + @Test + fun `should return null when URI exceeds maxUrlLength`() { + val longUri = "a/" + "x".repeat(2048) + matcher("a/{id}", maxUrlLength = 2048).match(longUri) shouldBe null + } + + @Test + fun `should match URI at exactly maxUrlLength`() { + // URI of exactly maxUrlLength characters must be accepted + val uri = "a/" + "x".repeat(2046) // length = 2048 + matcher("a/{id}", maxUrlLength = 2048).match(uri).shouldNotBeNull() + } + + // endregion + // region:Edge cases + + @Test + fun `should match root-level single segment template`() { + val result = matcher("{id}").match("42") + result.shouldNotBeNull { + variables["id"] shouldBe "42" + } + } + + @Test + fun `should treat leading and trailing slashes as equivalent`() { + val result = matcher("/users/{id}/").match("/users/7/") + result.shouldNotBeNull { + variables["id"] shouldBe "7" + } + } + + @Test + fun `should capture empty string for single-segment variable when URI is empty`() { + // "".trim('/').split("/") == [""] — one segment — so {id} captures "" + val result = matcher("{id}").match("") + result.shouldNotBeNull { + variables["id"] shouldBe "" + } + } + + @Test + fun `factory creates matcher equal to direct construction`() { + val template = ResourceTemplate("items/{id}", "Items") + val fromFactory = PathSegmentTemplateMatcher.factory.create(template) + val direct = PathSegmentTemplateMatcher(template) + + val uriToMatch = "items/99" + fromFactory.match(uriToMatch) shouldBe direct.match(uriToMatch) + } + + // endregion + // region: MatchResult equality + + @Test + fun `MatchResult equals by value`() { + val a = MatchResult(mapOf("id" to "1"), score = 3) + val b = MatchResult(mapOf("id" to "1"), score = 3) + a shouldBe b + } + + @Test + fun `MatchResult not equal when score differs`() { + val a = MatchResult(mapOf("id" to "1"), score = 3) + val b = MatchResult(mapOf("id" to "1"), score = 2) + (a == b) shouldBe false + } + + @Test + fun `MatchResult not equal when variables differ`() { + val a = MatchResult(mapOf("id" to "1"), score = 3) + val b = MatchResult(mapOf("id" to "2"), score = 3) + (a == b) shouldBe false + } + + // endregion + // region: Helpers + + private fun matcher( + uriTemplate: String, + maxUrlLength: Int = 2048, + maxDepth: Int = 10, + ): PathSegmentTemplateMatcher = PathSegmentTemplateMatcher( + resourceTemplate = ResourceTemplate(uriTemplate, "Test"), + maxUriLength = maxUrlLength, + maxDepth = maxDepth, + ) + // endregion +} diff --git a/kotlin-sdk-core/src/jsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.js.kt b/kotlin-sdk-core/src/jsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.js.kt new file mode 100644 index 0000000..fb8aaf4 --- /dev/null +++ b/kotlin-sdk-core/src/jsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.js.kt @@ -0,0 +1,16 @@ +package io.modelcontextprotocol.kotlin.sdk.utils + +private external class URL(url: String) { + val href: String +} + +/** + * Normalizes [uri] via the browser/Node.js `URL` API, resolving dot segments and + * normalizing the scheme, host, and path. Returns [uri] unchanged if it cannot be + * parsed (e.g. relative URIs or schemes unsupported by the runtime). + */ +internal actual fun normalizeUri(uri: String): String = try { + URL(uri).href +} catch (_: Throwable) { + uri +} diff --git a/kotlin-sdk-core/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.jvm.kt b/kotlin-sdk-core/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.jvm.kt index d00c1ab..35a1e98 100644 --- a/kotlin-sdk-core/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.jvm.kt +++ b/kotlin-sdk-core/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.jvm.kt @@ -3,5 +3,6 @@ package io.modelcontextprotocol.kotlin.sdk.internal import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +/** Platform-specific [CoroutineDispatcher] for I/O-bound operations. */ public actual val IODispatcher: CoroutineDispatcher get() = Dispatchers.IO diff --git a/kotlin-sdk-core/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.jvm.kt b/kotlin-sdk-core/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.jvm.kt new file mode 100644 index 0000000..0cde506 --- /dev/null +++ b/kotlin-sdk-core/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.jvm.kt @@ -0,0 +1,14 @@ +package io.modelcontextprotocol.kotlin.sdk.utils + +import java.net.URI +import java.net.URISyntaxException + +/** + * Normalizes [uri] by resolving dot segments (e.g. `/a/b/../c` → `/a/c`) via [URI.normalize]. + * Returns [uri] unchanged if it is not a valid URI. + */ +internal actual fun normalizeUri(uri: String): String = try { + URI(uri).normalize().toString() +} catch (_: URISyntaxException) { + uri +} diff --git a/kotlin-sdk-core/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransportTest.kt b/kotlin-sdk-core/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransportTest.kt new file mode 100644 index 0000000..1d144d8 --- /dev/null +++ b/kotlin-sdk-core/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransportTest.kt @@ -0,0 +1,498 @@ +package io.modelcontextprotocol.kotlin.sdk.shared + +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import io.mockk.spyk +import io.modelcontextprotocol.kotlin.sdk.InternalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.PingRequest +import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import io.modelcontextprotocol.kotlin.sdk.types.toJSON +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import org.junit.jupiter.params.provider.EnumSource +import org.junit.jupiter.params.provider.MethodSource +import kotlin.concurrent.atomics.ExperimentalAtomicApi +import kotlin.coroutines.cancellation.CancellationException + +/** + * Unit tests for [AbstractClientTransport] state machine and lifecycle. + * + * Tests behavioral correctness of state transitions, lifecycle management, + * and message sending without relying on mocking frameworks. + */ +@OptIn(ExperimentalAtomicApi::class) +class AbstractClientTransportTest { + + companion object { + @JvmStatic + fun nonMcpExceptions() = listOf( + TestException("Generic failure"), + java.io.IOException("Network error"), + IllegalStateException("Invalid state"), + ) + } + + private lateinit var transport: TestClientTransport + + @BeforeEach + fun beforeEach() { + transport = spyk(TestClientTransport()) + } + + @Nested + @DisplayName("Lifecycle") + inner class LifecycleTests { + + @Test + fun `should start with NEW state`() { + transport.currentState shouldBe ClientTransportState.New + } + + @Test + fun `should transition to OPERATIONAL after successful start`() = runTest { + transport.start() + + transport.currentState shouldBe ClientTransportState.Operational + transport.initializeCalled shouldBe true + } + + @Test + fun `should call initialize during start`() = runTest { + transport.start() + + transport.initializeCalled shouldBe true + transport.currentState shouldBe ClientTransportState.Operational + } + + @Test + fun `should transition to INITIALIZATION_FAILED on start error`() = runTest { + transport.shouldFailInitialization = true + + shouldThrow { + transport.start() + } + + transport.currentState shouldBe ClientTransportState.InitializationFailed + transport.closeResourcesCalled shouldBe true + } + + @Test + fun `should call closeResources on initialization failure`() = runTest { + transport.shouldFailInitialization = true + + shouldThrow { + transport.start() + } + + transport.closeResourcesCalled shouldBe true + } + + @Test + fun `should reject starting twice`() = runTest { + transport.start() + + val exception = shouldThrow { + transport.start() + } + exception.message shouldContain "expected transport state New" + } + + @Test + fun `should transition to STOPPED after successful close`() = runTest { + transport.start() + transport.close() + + transport.currentState shouldBe ClientTransportState.Stopped + transport.closeResourcesCalled shouldBe true + } + + @Test + fun `should call closeResources during close`() = runTest { + transport.start() + + transport.close() + + transport.closeResourcesCalled shouldBe true + transport.currentState shouldBe ClientTransportState.Stopped + } + + @Test + fun `should be idempotent when closed multiple times`() = runTest { + // given + transport.start() + transport.close() + // when + transport.close() + transport.close() + + transport.currentState shouldBe ClientTransportState.Stopped + transport.closeResourcesCallCount shouldBe 1 + } + + @Test + fun `should call onClose callback exactly once on multiple close calls`() = runTest { + var onCloseCallCount = 0 + transport.onClose { onCloseCallCount++ } + + transport.start() + transport.close() + transport.close() + + onCloseCallCount shouldBe 1 + } + + @Test + fun `should transition to SHUTDOWN_FAILED on close error`() = runTest { + transport.shouldFailClose = true + transport.start() + + transport.close() + + transport.currentState shouldBe ClientTransportState.ShutdownFailed + } + + @Test + fun `should call onClose even when close fails`() = runTest { + // given + var onCloseCalled = false + transport.onClose { onCloseCalled = true } + transport.shouldFailClose = true + transport.start() + // when + transport.close() + // then + onCloseCalled shouldBe true + } + + @Test + fun `should propagate CancellationException on close`() = runTest { + // given + transport.shouldThrowCancellation = true + transport.start() + // when-then + shouldThrow { + transport.close() + } + + transport.currentState shouldBe ClientTransportState.ShutdownFailed + } + + @Test + fun `should call onClose even when CancellationException is thrown`() = runTest { + var onCloseCalled = false + transport.onClose { onCloseCalled = true } + transport.shouldThrowCancellation = true + transport.start() + + shouldThrow { + transport.close() + } + + onCloseCalled shouldBe true + } + } + + @Nested + @DisplayName("State Transitions") + inner class StateTransitionTests { + + @ParameterizedTest + @CsvSource( + "New, Initializing", + "New, Stopped", + "Initializing, Operational", + "Initializing, InitializationFailed", + "Operational, ShuttingDown", + "ShuttingDown, Stopped", + "ShuttingDown, ShutdownFailed", + ) + fun `should allow valid transitions`(fromName: String, toName: String) { + val from = ClientTransportState.valueOf(fromName) + val to = ClientTransportState.valueOf(toName) + + transport.forceState(from) + transport.testStateTransition(from, to) + + transport.currentState shouldBe to + } + + @ParameterizedTest + @CsvSource( + // From New: only Initializing is valid + "New, Operational", + "New, InitializationFailed", + "New, ShuttingDown", + "New, ShutdownFailed", + // From Initializing: only Operational or InitializationFailed are valid + "Initializing, New", + "Initializing, Initializing", + "Initializing, ShuttingDown", + "Initializing, Stopped", + "Initializing, ShutdownFailed", + // From Operational: only ShuttingDown is valid + "Operational, New", + "Operational, Initializing", + "Operational, InitializationFailed", + "Operational, Operational", + "Operational, Stopped", + "Operational, ShutdownFailed", + // From ShuttingDown: only Stopped or ShutdownFailed are valid + "ShuttingDown, New", + "ShuttingDown, Initializing", + "ShuttingDown, InitializationFailed", + "ShuttingDown, Operational", + "ShuttingDown, ShuttingDown", + // From terminal states: no transitions allowed + "InitializationFailed, New", + "InitializationFailed, Operational", + "Stopped, New", + "Stopped, Operational", + "ShutdownFailed, New", + "ShutdownFailed, Operational", + ) + fun `should reject invalid transitions`(fromName: String, toName: String) { + val from = ClientTransportState.valueOf(fromName) + val to = ClientTransportState.valueOf(toName) + + transport.forceState(from) + + val exception = shouldThrow { + transport.testStateTransition(from, to) + } + exception.message shouldContain "Invalid transition: $from → $to" + } + } + + @Nested + @DisplayName("Send Operations") + inner class SendTests { + + @Test + fun `should send message successfully when OPERATIONAL`() = runTest { + val message = PingRequest().toJSON() + + transport.start() + transport.send(message) + + transport.sentMessages shouldBe listOf(message) + } + + @Test + fun `should pass options to performSend`() = runTest { + val message = PingRequest().toJSON() + val options = TransportSendOptions() + + transport.start() + transport.send(message, options) + + transport.sentMessages shouldBe listOf(message) + transport.lastSendOptions shouldBe options + } + + @Test + fun `should throw when sending before start`() = runTest { + val exception = shouldThrow { + transport.send(PingRequest().toJSON()) + } + + exception.code shouldBe RPCError.ErrorCode.CONNECTION_CLOSED + exception.message shouldContain "Transport is not ready" + } + + @Test + fun `should throw when sending after close`() = runTest { + transport.start() + transport.close() + + val exception = shouldThrow { + transport.send(PingRequest().toJSON()) + } + + exception.code shouldBe RPCError.ErrorCode.CONNECTION_CLOSED + exception.message shouldContain "Transport is not ready" + } + + @ParameterizedTest + @EnumSource( + value = ClientTransportState::class, + names = ["Operational"], + mode = EnumSource.Mode.EXCLUDE, + ) + fun `should throw when sending in non-OPERATIONAL state`(state: ClientTransportState) = runTest { + transport.forceState(state) + + val exception = shouldThrow { + transport.send(PingRequest().toJSON()) + } + + exception.code shouldBe RPCError.ErrorCode.CONNECTION_CLOSED + exception.message shouldContain "Transport is not ready" + transport.sentMessages.isEmpty() shouldBe true + } + + @Test + fun `should call onError when performSend throws McpException`() = runTest { + var errorCallCount = 0 + var capturedError: Throwable? = null + transport.onError { error -> + errorCallCount++ + capturedError = error + } + transport.shouldFailSend = true + transport.sendException = McpException(RPCError.ErrorCode.INTERNAL_ERROR, "Send failed") + + transport.start() + val exception = shouldThrow { + transport.send(PingRequest().toJSON()) + } + + errorCallCount shouldBe 1 + capturedError shouldBe transport.sendException + exception shouldBe transport.sendException + } + + @ParameterizedTest + @MethodSource("io.modelcontextprotocol.kotlin.sdk.shared.AbstractClientTransportTest#nonMcpExceptions") + fun `should call onError when performSend throws non-MCP exception`(testException: Throwable) = runTest { + var errorCallCount = 0 + var capturedError: Throwable? = null + transport.onError { error -> + errorCallCount++ + capturedError = error + } + transport.shouldFailSend = true + transport.sendException = testException + + transport.start() + val exception = shouldThrow { + transport.send(PingRequest().toJSON()) + } + + errorCallCount shouldBe 1 + capturedError shouldBe testException + exception.code shouldBe RPCError.ErrorCode.INTERNAL_ERROR + exception.cause shouldBe testException + } + + @Test + fun `should NOT call onError when performSend throws CancellationException`() = runTest { + var errorCallCount = 0 + transport.onError { errorCallCount++ } + transport.shouldFailSend = true + transport.sendException = CancellationException("Operation cancelled") + + transport.start() + shouldThrow { + transport.send(PingRequest().toJSON()) + } + + errorCallCount shouldBe 0 + } + } + + @Nested + @DisplayName("Idempotency") + inner class IdempotencyTests { + + @Test + fun `should be idempotent when closing from NEW state`() = runTest { + transport.close() + + transport.currentState shouldBe ClientTransportState.Stopped + transport.closeResourcesCalled shouldBe false + } + + @Test + fun `should be idempotent when closing from INITIALIZATION_FAILED state`() = runTest { + transport.shouldFailInitialization = true + shouldThrow { transport.start() } + + transport.close() + + transport.currentState shouldBe ClientTransportState.InitializationFailed + // closeResources was called during a failed start, but not during this close + transport.closeResourcesCallCount shouldBe 1 + } + + @ParameterizedTest + @EnumSource( + value = ClientTransportState::class, + names = ["ShuttingDown", "ShutdownFailed", "Stopped"], + ) + fun `should be idempotent when closing from terminal or shutdown states`(state: ClientTransportState) = + runTest { + transport.forceState(state) + val initialCloseCount = transport.closeResourcesCallCount + + transport.close() + + transport.currentState shouldBe state + transport.closeResourcesCallCount shouldBe initialCloseCount + } + } + + /** + * Simple test implementation of [AbstractClientTransport] for behavioral testing. + * Tracks method calls without mocking frameworks. + */ + @OptIn(InternalMcpApi::class) + private class TestClientTransport : AbstractClientTransport() { + override val logger: KLogger = KotlinLogging.logger {} + val sentMessages = mutableListOf() + var lastSendOptions: TransportSendOptions? = null + var initializeCalled = false + var closeResourcesCalled = false + var closeResourcesCallCount = 0 + var shouldFailInitialization = false + var shouldFailClose = false + var shouldThrowCancellation = false + var shouldFailSend = false + var sendException: Throwable? = null + + val currentState: ClientTransportState + get() = state + + override suspend fun initialize() { + initializeCalled = true + if (shouldFailInitialization) { + throw TestException("Initialization failed") + } + } + + override suspend fun performSend(message: JSONRPCMessage, options: TransportSendOptions?) { + if (shouldFailSend) { + throw sendException ?: TestException("Send failed") + } + sentMessages.add(message) + lastSendOptions = options + } + + override suspend fun closeResources() { + closeResourcesCalled = true + closeResourcesCallCount++ + + when { + shouldThrowCancellation -> throw CancellationException("Test cancellation") + shouldFailClose -> throw TestException("Close failed") + } + } + + fun testStateTransition(from: ClientTransportState, to: ClientTransportState) { + stateTransition(from, to) + } + + fun forceState(newState: ClientTransportState) = updateState(newState) + } + + private class TestException(message: String) : Exception(message) +} diff --git a/kotlin-sdk-core/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolConcurrencyJvmTest.kt b/kotlin-sdk-core/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolConcurrencyJvmTest.kt new file mode 100644 index 0000000..60658f8 --- /dev/null +++ b/kotlin-sdk-core/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolConcurrencyJvmTest.kt @@ -0,0 +1,114 @@ +package io.modelcontextprotocol.kotlin.sdk.shared + +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse +import io.modelcontextprotocol.kotlin.sdk.types.RequestId +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.test.Test +import kotlin.time.Duration.Companion.seconds + +/** + * Real-dispatcher counterparts to [ProtocolConcurrencyTest]. + * + * These run under [runBlocking] (real time, real threads on [Dispatchers.Default]) rather than + * `runTest`: a virtual-time scheduler auto-advances past the `withTimeout` guards and cannot + * observe genuine cross-thread parallelism, so the concurrency assertions could pass vacuously. + */ +class ProtocolConcurrencyJvmTest { + + @Test + fun `fast request completes while a slow handler is parked on Dispatchers Default`() = runBlocking { + val protocol = TestProtocol(ProtocolOptions(handlerCoroutineContext = Dispatchers.Default)) + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + val slowEntered = CompletableDeferred() + val releaseSlow = CompletableDeferred() + protocol.fallbackRequestHandler = { request, _ -> + if (request.method == "test/slow") { + slowEntered.complete(Unit) + releaseSlow.await() + } + EmptyResult() + } + + transport.deliver(JSONRPCRequest(id = RequestId(1L), method = "test/slow")) + withTimeout(5.seconds) { slowEntered.await() } + transport.deliver(JSONRPCRequest(id = RequestId(2L), method = "test/fast")) + + val fast = withTimeout(5.seconds) { transport.awaitSent() } + (fast as JSONRPCResponse).id shouldBe RequestId(2L) + + releaseSlow.complete(Unit) + val slow = withTimeout(5.seconds) { transport.awaitSent() } + (slow as JSONRPCResponse).id shouldBe RequestId(1L) + Unit + } + + @Test + fun `dispatch stays serial before the gate flips on Dispatchers Default`() = runBlocking { + val protocol = TestProtocol(ProtocolOptions(handlerCoroutineContext = Dispatchers.Default)) + val transport = RecordingTransport() + protocol.connect(transport) + // gate NOT flipped + + val slowEntered = CompletableDeferred() + val releaseSlow = CompletableDeferred() + protocol.fallbackRequestHandler = { request, _ -> + if (request.method == "test/slow") { + slowEntered.complete(Unit) + releaseSlow.await() + } + EmptyResult() + } + + // A single delivering coroutine models the transport read loop: the second deliver cannot + // start until the first returns, so a parked slow handler blocks the fast request. + val delivery = launch { + transport.deliver(JSONRPCRequest(id = RequestId(1L), method = "test/slow")) + transport.deliver(JSONRPCRequest(id = RequestId(2L), method = "test/fast")) + } + withTimeout(5.seconds) { slowEntered.await() } + + // The delivering coroutine is parked inside the slow handler; fast was never delivered. + transport.sentMessages.tryReceive().isFailure shouldBe true + + releaseSlow.complete(Unit) + val first = withTimeout(5.seconds) { transport.awaitSent() } + (first as JSONRPCResponse).id shouldBe RequestId(1L) + val second = withTimeout(5.seconds) { transport.awaitSent() } + (second as JSONRPCResponse).id shouldBe RequestId(2L) + delivery.join() + } + + @Test + fun `suspension-free notification handlers keep arrival order on Dispatchers Default`() = runBlocking { + val protocol = TestProtocol(ProtocolOptions(handlerCoroutineContext = Dispatchers.Default)) + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + // safe: appended only from the delivering coroutine (UNDISPATCHED, suspension-free) + val seen = mutableListOf() + protocol.fallbackNotificationHandler = { notification -> + seen.add((notification.params as JsonObject).getValue("i").jsonPrimitive.int) + } + repeat(100) { i -> + transport.deliver(JSONRPCNotification(method = "test/event", params = buildJsonObject { put("i", i) })) + } + seen shouldBe (0 until 100).toList() + } +} diff --git a/kotlin-sdk-core/src/jvmTest/resources/junit-platform.properties b/kotlin-sdk-core/src/jvmTest/resources/junit-platform.properties new file mode 100644 index 0000000..9af7987 --- /dev/null +++ b/kotlin-sdk-core/src/jvmTest/resources/junit-platform.properties @@ -0,0 +1,6 @@ +## https://docs.junit.org/5.3.0-M1/user-guide/index.html#writing-tests-parallel-execution +junit.jupiter.execution.parallel.enabled=true +junit.jupiter.execution.parallel.config.strategy=dynamic +junit.jupiter.execution.parallel.mode.default=concurrent +junit.jupiter.execution.parallel.mode.classes.default=concurrent +junit.jupiter.execution.timeout.default=2m diff --git a/kotlin-sdk-core/src/jvmTest/resources/simplelogger.properties b/kotlin-sdk-core/src/jvmTest/resources/simplelogger.properties new file mode 100644 index 0000000..95fb646 --- /dev/null +++ b/kotlin-sdk-core/src/jvmTest/resources/simplelogger.properties @@ -0,0 +1,8 @@ +# Level of logging for the ROOT logger: ERROR, WARN, INFO, DEBUG, TRACE (default is INFO) +org.slf4j.simpleLogger.defaultLogLevel=INFO +org.slf4j.simpleLogger.showThreadName=true +org.slf4j.simpleLogger.showDateTime=false + +# Log level for specific packages or classes +org.slf4j.simpleLogger.log.io.ktor.server=DEBUG +org.slf4j.simpleLogger.log.io.modelcontextprotocol=DEBUG diff --git a/kotlin-sdk-core/src/nativeMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.native.kt b/kotlin-sdk-core/src/nativeMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.native.kt new file mode 100644 index 0000000..623fdfc --- /dev/null +++ b/kotlin-sdk-core/src/nativeMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.native.kt @@ -0,0 +1,3 @@ +package io.modelcontextprotocol.kotlin.sdk.utils +/** No platform-native URI normalizer is available on native targets; [uri] is returned unchanged. */ +internal actual fun normalizeUri(uri: String): String = uri diff --git a/kotlin-sdk-core/src/wasmJsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.wasmJs.kt b/kotlin-sdk-core/src/wasmJsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.wasmJs.kt new file mode 100644 index 0000000..7ad7bce --- /dev/null +++ b/kotlin-sdk-core/src/wasmJsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.wasmJs.kt @@ -0,0 +1,4 @@ +package io.modelcontextprotocol.kotlin.sdk.utils + +/** No platform-native URI normalizer is available on WASM targets; [uri] is returned unchanged. */ +internal actual fun normalizeUri(uri: String): String = uri diff --git a/kotlin-sdk-server/Module.md b/kotlin-sdk-server/Module.md index 94ae71e..176a398 100644 --- a/kotlin-sdk-server/Module.md +++ b/kotlin-sdk-server/Module.md @@ -1,14 +1,125 @@ # Module kotlin-sdk-server -The server module provides core server-side functionality for the Model Context Protocol (MCP) Kotlin SDK. -This module includes essential parts for writing MCP server applications. +`kotlin-sdk-server` contains the server-side building blocks for Model Context Protocol (MCP) applications written in +Kotlin Multiplatform. It pairs the protocol types from `kotlin-sdk-core` with transport implementations and utilities +that make it straightforward to expose tools, prompts, and resources to MCP clients. -## Features +## What the module provides -* Server-side request handling -* Model context management -* Protocol implementation -* Integration utilities +- **Server runtime**: The `Server` class coordinates sessions, initialization flow, and capability enforcement. It + tracks active sessions and cleans them up when transports close. +- **Feature registries**: Helpers to register and remove tools, prompts, resources, and resource templates. Registry + listeners emit list-changed notifications when the capability is enabled. +- **Transports**: Ready-to-use transports for STDIO (CLI-style hosting), Ktor Server-Sent Events (SSE) with POST + back-channel, and Ktor WebSocket. All share the common `Transport` abstraction from the SDK. +- **Ktor integration**: Extension functions (`Route.mcp`, `Route.mcpWebSocket`, and `Application` variants) that + plug MCP into an existing Ktor server with minimal boilerplate. +- **Notifications**: Built-in support for resource subscription, resource-updated events, and feature list change + notifications when capabilities are set accordingly. -For detailed usage instructions and examples, please refer to -the [documentation](https://github.com/modelcontextprotocol/kotlin-sdk/blob/main/README.md). +## Typical server setup + +1. Declare capabilities your server supports using `ServerOptions`. Capabilities drive which handlers are installed and + whether list-change notifications are emitted. +2. Register features: tools, prompts, resources, and resource templates. Handlers run in suspending context and can be + removed or replaced at runtime. +3. Attach a transport and open a session via `createSession(transport)`. Multiple sessions can be active simultaneously. +4. Close the server to tear down transports and unsubscribe sessions. + +### Minimal STDIO server + +```kotlin +val server = Server( + serverInfo = Implementation(name = "demo-server", version = "1.0.0"), + options = ServerOptions( + capabilities = ServerCapabilities( + tools = ServerCapabilities.Tools(listChanged = true) + ) + ) +) { + addTool( + name = "hello", + description = "Greets the caller by name" + ) { request -> + val name = request.params.arguments?.get("name")?.jsonPrimitive?.content ?: "there" + CallToolResult( + content = listOf(TextContent("Hello, $name!")), + isError = false, + ) + } +} + +val transport = StdioServerTransport( + System.`in`.asSource().buffered(), + System.out.asSink().buffered() +) +server.createSession(transport) +``` + +### Hosting with Ktor (SSE) + +```kotlin +fun Application.module() { + mcp { + Server( + serverInfo = Implementation("ktor-sse", "1.0.0"), + options = ServerOptions( + ServerCapabilities( + prompts = ServerCapabilities.Prompts(listChanged = true), + resources = ServerCapabilities.Resources(listChanged = true, subscribe = true), + ) + ) + ) { + addPrompt( + name = "welcome", + description = "Explain how to use the server" + ) { + GetPromptResult( + description = "Welcome to the MCP server", + messages = listOf(CreateMessageResult.Message(TextContent("Try listing tools first"))) + ) + } + } + } +} +``` + +The SSE route exposes a streaming GET endpoint and a POST endpoint for client messages. Session affinity is maintained +by a `sessionId` query parameter managed by the SDK. + +### Hosting with Ktor (WebSocket) + +```kotlin +fun Application.module() { + mcpWebSocket { + Server( + serverInfo = Implementation("ktor-ws", "1.0.0"), + options = ServerOptions(ServerCapabilities(tools = ServerCapabilities.Tools())) + ) + } +} +``` + +`mcpWebSocket` installs Ktor WebSockets and wires message handling over a single bidirectional connection. + +## Feature registration details + +- **Tools**: `addTool` accepts a `Tool` descriptor or inline parameters plus a suspend handler. `removeTool` and + `addTools/removeTools` support dynamic updates. +- **Prompts**: `addPrompt` wires a provider that returns `GetPromptResult` and optional arguments metadata. +- **Resources**: `addResource` exposes static or generated content via `ReadResourceResult`; `addResources` + batch-registers. Templates and roots are also supported for structured discovery. +- **Instructions**: Provide static text or a function via `instructionsProvider` to send per-session usage guidance + during initialization. + +## Lifecycle and diagnostics + +- Sessions emit callbacks via `onConnect` and `onClose`. Deprecated `onInitialized` exists for backward compatibility; + prefer session-level hooks. +- The server enforces declared capabilities; attempting to register an unsupported feature throws with a descriptive + message. +- Logging uses `KotlinLogging`; avoid logging secrets. To inspect traffic, run the server with DEBUG level enabled. + +Use this module whenever you need to expose MCP features from a Kotlin service—whether embedding in a CLI via STDIO, +serving from Ktor over SSE/WebSocket, or composing transports for custom environments. Public APIs are explicit to keep +server-facing contracts stable for consumers. diff --git a/kotlin-sdk-server/api/kotlin-sdk-server.api b/kotlin-sdk-server/api/kotlin-sdk-server.api index ec09fea..d8e4c7a 100644 --- a/kotlin-sdk-server/api/kotlin-sdk-server.api +++ b/kotlin-sdk-server/api/kotlin-sdk-server.api @@ -1,119 +1,283 @@ +public abstract interface class io/modelcontextprotocol/kotlin/sdk/server/ClientConnection { + public abstract fun createElicitation (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun createElicitation (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun createElicitation (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public abstract fun createMessage (Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun createMessage$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public abstract fun getSessionId ()Ljava/lang/String; + public abstract fun listRoots (Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun listRoots$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public abstract fun notification (Lio/modelcontextprotocol/kotlin/sdk/types/ServerNotification;Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun notification$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/ServerNotification;Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public abstract fun ping (Lio/modelcontextprotocol/kotlin/sdk/types/PingRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun ping$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/PingRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public abstract fun sendElicitationComplete (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun sendLoggingMessage (Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun sendPromptListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun sendResourceListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun sendResourceUpdated (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun sendToolListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class io/modelcontextprotocol/kotlin/sdk/server/ClientConnection$DefaultImpls { + public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun createMessage$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun listRoots$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun notification$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/ServerNotification;Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun ping$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/PingRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; +} + +public final class io/modelcontextprotocol/kotlin/sdk/server/DnsRebindingProtectionConfig { + public fun ()V + public final fun getAllowedHosts ()Ljava/util/List; + public final fun getAllowedOrigins ()Ljava/util/List; + public final fun setAllowedHosts (Ljava/util/List;)V + public final fun setAllowedOrigins (Ljava/util/List;)V +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/server/EventStore { + public abstract fun getStreamIdForEventId (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun replayEventsAfter (Ljava/lang/String;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun storeEvent (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class io/modelcontextprotocol/kotlin/sdk/server/HostValidationKt { + public static final fun getDnsRebindingProtection ()Lio/ktor/server/application/RouteScopedPlugin; +} + public final class io/modelcontextprotocol/kotlin/sdk/server/KtorServerKt { - public static final fun MCP (Lio/ktor/server/application/Application;Lkotlin/jvm/functions/Function1;)V - public static final fun mcp (Lio/ktor/server/application/Application;Lkotlin/jvm/functions/Function1;)V - public static final fun mcp (Lio/ktor/server/routing/Routing;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V - public static final fun mcp (Lio/ktor/server/routing/Routing;Lkotlin/jvm/functions/Function1;)V + public static final fun mcp (Lio/ktor/server/application/Application;ZLjava/util/List;Ljava/util/List;JLkotlin/jvm/functions/Function1;)V + public static final fun mcp (Lio/ktor/server/routing/Route;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;JLkotlin/jvm/functions/Function1;)V + public static final fun mcp (Lio/ktor/server/routing/Route;ZLjava/util/List;Ljava/util/List;JLkotlin/jvm/functions/Function1;)V + public static synthetic fun mcp$default (Lio/ktor/server/application/Application;ZLjava/util/List;Ljava/util/List;JLkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static synthetic fun mcp$default (Lio/ktor/server/routing/Route;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;JLkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static synthetic fun mcp$default (Lio/ktor/server/routing/Route;ZLjava/util/List;Ljava/util/List;JLkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static final fun mcpStatelessStreamableHttp (Lio/ktor/server/application/Application;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/jvm/functions/Function1;)V + public static final fun mcpStatelessStreamableHttp (Lio/ktor/server/application/Application;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun mcpStatelessStreamableHttp$default (Lio/ktor/server/application/Application;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static synthetic fun mcpStatelessStreamableHttp$default (Lio/ktor/server/application/Application;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static final fun mcpStreamableHttp (Lio/ktor/server/application/Application;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun mcpStreamableHttp$default (Lio/ktor/server/application/Application;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V } -public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt { - public fun (Lio/modelcontextprotocol/kotlin/sdk/Prompt;Lkotlin/jvm/functions/Function2;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/Prompt; - public final fun component2 ()Lkotlin/jvm/functions/Function2; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/Prompt;Lkotlin/jvm/functions/Function2;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt;Lio/modelcontextprotocol/kotlin/sdk/Prompt;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt; +public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt : io/modelcontextprotocol/kotlin/sdk/server/Feature { + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/Prompt;Lkotlin/jvm/functions/Function3;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/Prompt; + public final fun component2 ()Lkotlin/jvm/functions/Function3; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/Prompt;Lkotlin/jvm/functions/Function3;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt;Lio/modelcontextprotocol/kotlin/sdk/types/Prompt;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt; public fun equals (Ljava/lang/Object;)Z - public final fun getMessageProvider ()Lkotlin/jvm/functions/Function2; - public final fun getPrompt ()Lio/modelcontextprotocol/kotlin/sdk/Prompt; + public fun getKey ()Ljava/lang/String; + public final fun getMessageProvider ()Lkotlin/jvm/functions/Function3; + public final fun getPrompt ()Lio/modelcontextprotocol/kotlin/sdk/types/Prompt; public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredResource { - public fun (Lio/modelcontextprotocol/kotlin/sdk/Resource;Lkotlin/jvm/functions/Function2;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/Resource; - public final fun component2 ()Lkotlin/jvm/functions/Function2; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/Resource;Lkotlin/jvm/functions/Function2;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredResource; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredResource;Lio/modelcontextprotocol/kotlin/sdk/Resource;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredResource; +public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredResource : io/modelcontextprotocol/kotlin/sdk/server/Feature { + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/Resource;Lkotlin/jvm/functions/Function3;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/Resource; + public final fun component2 ()Lkotlin/jvm/functions/Function3; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/Resource;Lkotlin/jvm/functions/Function3;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredResource; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredResource;Lio/modelcontextprotocol/kotlin/sdk/types/Resource;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredResource; public fun equals (Ljava/lang/Object;)Z - public final fun getReadHandler ()Lkotlin/jvm/functions/Function2; - public final fun getResource ()Lio/modelcontextprotocol/kotlin/sdk/Resource; + public fun getKey ()Ljava/lang/String; + public final fun getReadHandler ()Lkotlin/jvm/functions/Function3; + public final fun getResource ()Lio/modelcontextprotocol/kotlin/sdk/types/Resource; public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredTool { - public fun (Lio/modelcontextprotocol/kotlin/sdk/Tool;Lkotlin/jvm/functions/Function2;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/Tool; - public final fun component2 ()Lkotlin/jvm/functions/Function2; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/Tool;Lkotlin/jvm/functions/Function2;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredTool; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredTool;Lio/modelcontextprotocol/kotlin/sdk/Tool;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredTool; +public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredTool : io/modelcontextprotocol/kotlin/sdk/server/Feature { + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/Tool;Lkotlin/jvm/functions/Function3;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/Tool; + public final fun component2 ()Lkotlin/jvm/functions/Function3; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/Tool;Lkotlin/jvm/functions/Function3;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredTool; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredTool;Lio/modelcontextprotocol/kotlin/sdk/types/Tool;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredTool; public fun equals (Ljava/lang/Object;)Z - public final fun getHandler ()Lkotlin/jvm/functions/Function2; - public final fun getTool ()Lio/modelcontextprotocol/kotlin/sdk/Tool; + public final fun getHandler ()Lkotlin/jvm/functions/Function3; + public fun getKey ()Ljava/lang/String; + public final fun getTool ()Lio/modelcontextprotocol/kotlin/sdk/types/Tool; public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public class io/modelcontextprotocol/kotlin/sdk/server/Server : io/modelcontextprotocol/kotlin/sdk/shared/Protocol { - public fun (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;)V - public final fun addPrompt (Lio/modelcontextprotocol/kotlin/sdk/Prompt;Lkotlin/jvm/functions/Function2;)V - public final fun addPrompt (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function2;)V - public static synthetic fun addPrompt$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V +public class io/modelcontextprotocol/kotlin/sdk/server/Server { + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun addPrompt (Lio/modelcontextprotocol/kotlin/sdk/types/Prompt;Lkotlin/jvm/functions/Function3;)V + public final fun addPrompt (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function3;)V + public static synthetic fun addPrompt$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)V public final fun addPrompts (Ljava/util/List;)V - public final fun addResource (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;)V - public static synthetic fun addResource$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public final fun addResource (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function3;)V + public static synthetic fun addResource$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)V + public final fun addResourceTemplate (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate;Lkotlin/jvm/functions/Function4;)V + public final fun addResourceTemplate (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function4;)V + public static synthetic fun addResourceTemplate$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function4;ILjava/lang/Object;)V public final fun addResources (Ljava/util/List;)V - public final fun addTool (Lio/modelcontextprotocol/kotlin/sdk/Tool;Lkotlin/jvm/functions/Function2;)V - public final fun addTool (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;Lkotlin/jvm/functions/Function2;)V - public static synthetic fun addTool$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public final fun addTool (Lio/modelcontextprotocol/kotlin/sdk/types/Tool;Lkotlin/jvm/functions/Function3;)V + public final fun addTool (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations;Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/functions/Function3;)V + public static synthetic fun addTool$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations;Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)V public final fun addTools (Ljava/util/List;)V - protected fun assertCapabilityForMethod (Lio/modelcontextprotocol/kotlin/sdk/Method;)V - protected fun assertNotificationCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V - public fun assertRequestHandlerCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V - public final fun createElicitation (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun createMessage (Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun createMessage$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun getClientCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities; - public final fun getClientVersion ()Lio/modelcontextprotocol/kotlin/sdk/Implementation; + public final fun clientConnection (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection; + public final fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun createElicitation (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun createElicitation (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun createMessage (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun createMessage$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun createSession (Lio/modelcontextprotocol/kotlin/sdk/shared/Transport;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + protected final fun getInstructionsProvider ()Lkotlin/jvm/functions/Function0; + protected final fun getOptions ()Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions; public final fun getPrompts ()Ljava/util/Map; + public final fun getResourceTemplates ()Ljava/util/List; public final fun getResources ()Ljava/util/Map; + protected final fun getServerInfo ()Lio/modelcontextprotocol/kotlin/sdk/types/Implementation; + public final fun getSessions ()Ljava/util/Map; public final fun getTools ()Ljava/util/Map; - public final fun listRoots (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun listRoots$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public fun onClose ()V + public final fun listRoots (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun listRoots$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final fun onClose (Lkotlin/jvm/functions/Function0;)V - public final fun onInitialized (Lkotlin/jvm/functions/Function0;)V - public final fun ping (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun onConnect (Lkotlin/jvm/functions/Function0;)V + public final fun ping (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun removeNotificationHandler (Lio/modelcontextprotocol/kotlin/sdk/types/Method;)V + public final fun removeNotificationHandler (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Method;)V public final fun removePrompt (Ljava/lang/String;)Z public final fun removePrompts (Ljava/util/List;)I public final fun removeResource (Ljava/lang/String;)Z + public final fun removeResourceTemplate (Ljava/lang/String;)Z public final fun removeResources (Ljava/util/List;)I public final fun removeTool (Ljava/lang/String;)Z public final fun removeTools (Ljava/util/List;)I - public final fun sendLoggingMessage (Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun sendPromptListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun sendResourceListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun sendResourceUpdated (Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun sendToolListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun sendElicitationComplete (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun sendLoggingMessage (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun sendPromptListChanged (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun sendResourceListChanged (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun sendResourceUpdated (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun sendToolListChanged (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun setNotificationHandler (Lio/modelcontextprotocol/kotlin/sdk/types/Method;Lkotlin/jvm/functions/Function1;)V + public final fun setNotificationHandler (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Method;Lkotlin/jvm/functions/Function1;)V } public final class io/modelcontextprotocol/kotlin/sdk/server/ServerOptions : io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptions { - public fun (Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;Z)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;)V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;Z)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;ZLio/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcherFactory;Lkotlin/coroutines/CoroutineContext;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;ZLio/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcherFactory;Lkotlin/coroutines/CoroutineContext;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities; + public final fun getResourceTemplateMatcherFactory ()Lio/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcherFactory; +} + +public class io/modelcontextprotocol/kotlin/sdk/server/ServerSession : io/modelcontextprotocol/kotlin/sdk/shared/Protocol { + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Ljava/lang/String;)V + protected fun assertCapabilityForMethod (Lio/modelcontextprotocol/kotlin/sdk/types/Method;)V + protected fun assertNotificationCapability (Lio/modelcontextprotocol/kotlin/sdk/types/Method;)V + public fun assertRequestHandlerCapability (Lio/modelcontextprotocol/kotlin/sdk/types/Method;)V + public final fun createElicitation (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun createElicitation (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/ServerSession;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/ServerSession;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun createMessage (Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun createMessage$default (Lio/modelcontextprotocol/kotlin/sdk/server/ServerSession;Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public fun equals (Ljava/lang/Object;)Z + public final fun getClientCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; + public final fun getClientVersion ()Lio/modelcontextprotocol/kotlin/sdk/types/Implementation; + protected final fun getInstructions ()Ljava/lang/String; + protected final fun getServerInfo ()Lio/modelcontextprotocol/kotlin/sdk/types/Implementation; + public final fun getSessionId ()Ljava/lang/String; + public fun hashCode ()I + public final fun listRoots (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun listRoots$default (Lio/modelcontextprotocol/kotlin/sdk/server/ServerSession;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public fun onClose ()V + public final fun onClose (Lkotlin/jvm/functions/Function0;)V + public final fun onInitialized (Lkotlin/jvm/functions/Function0;)V + protected fun onInitializedNotification ()V + public final fun ping (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun sendElicitationComplete (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun sendLoggingMessage (Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun sendPromptListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun sendResourceListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun sendResourceUpdated (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun sendToolListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public final class io/modelcontextprotocol/kotlin/sdk/server/SseServerTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { - public fun (Ljava/lang/String;Lio/ktor/server/sse/ServerSSESession;)V + public fun (Ljava/lang/String;Lio/ktor/server/sse/ServerSSESession;J)V + public synthetic fun (Ljava/lang/String;Lio/ktor/server/sse/ServerSSESession;JILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun getSessionId ()Ljava/lang/String; public final fun handleMessage (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun handlePostMessage (Lio/ktor/server/application/ApplicationCall;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun send (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Lio/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public final class io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { public fun (Lkotlinx/io/Source;Lkotlinx/io/Sink;)V + public fun (Lkotlinx/io/Source;Lkotlinx/io/Sink;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lkotlinx/io/Source;Lkotlinx/io/Sink;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun send (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Lio/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public final class io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport$Builder { + public final fun getHandlerDispatcher ()Lkotlinx/coroutines/CoroutineDispatcher; + public final fun getIoDispatcher ()Lkotlinx/coroutines/CoroutineDispatcher; + public final fun getScope ()Lkotlinx/coroutines/CoroutineScope; + public final fun setHandlerDispatcher (Lkotlinx/coroutines/CoroutineDispatcher;)V + public final fun setIoDispatcher (Lkotlinx/coroutines/CoroutineDispatcher;)V + public final fun setScope (Lkotlinx/coroutines/CoroutineScope;)V +} + +public final class io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { + public static final field STANDALONE_SSE_STREAM_ID Ljava/lang/String; + public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport$Configuration;)V + public fun (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Ljava/lang/Long;)V + public synthetic fun (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Ljava/lang/Long;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun closeSseStream (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun getSessionId ()Ljava/lang/String; + public final fun handleDeleteRequest (Lio/ktor/server/application/ApplicationCall;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun handleGetRequest (Lio/ktor/server/sse/ServerSSESession;Lio/ktor/server/application/ApplicationCall;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun handlePostRequest (Lio/ktor/server/sse/ServerSSESession;Lio/ktor/server/application/ApplicationCall;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun handleRequest (Lio/ktor/server/sse/ServerSSESession;Lio/ktor/server/application/ApplicationCall;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun send (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Lio/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun setOnSessionClosed (Lkotlin/jvm/functions/Function1;)V + public final fun setOnSessionInitialized (Lkotlin/jvm/functions/Function1;)V + public final fun setSessionIdGenerator (Lkotlin/jvm/functions/Function0;)V + public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport$Configuration { + public synthetic fun (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/time/Duration;JILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/time/Duration;JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getAllowedHosts ()Ljava/util/List; + public final fun getAllowedOrigins ()Ljava/util/List; + public final fun getEnableDnsRebindingProtection ()Z + public final fun getEnableJsonResponse ()Z + public final fun getEventStore ()Lio/modelcontextprotocol/kotlin/sdk/server/EventStore; + public final fun getMaxRequestBodySize ()J + public final fun getRetryInterval-FghU774 ()Lkotlin/time/Duration; +} + public final class io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensionsKt { + public static final fun mcpWebSocket (Lio/ktor/server/application/Application;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V + public static final fun mcpWebSocket (Lio/ktor/server/application/Application;Lkotlin/jvm/functions/Function0;)V public static final fun mcpWebSocket (Lio/ktor/server/routing/Route;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function2;)V public static final fun mcpWebSocket (Lio/ktor/server/routing/Route;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function2;)V + public static final fun mcpWebSocket (Lio/ktor/server/routing/Route;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V + public static final fun mcpWebSocket (Lio/ktor/server/routing/Route;Lkotlin/jvm/functions/Function0;)V public static synthetic fun mcpWebSocket$default (Lio/ktor/server/routing/Route;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V public static synthetic fun mcpWebSocket$default (Lio/ktor/server/routing/Route;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V public static final fun mcpWebSocketTransport (Lio/ktor/server/routing/Route;Ljava/lang/String;Lkotlin/jvm/functions/Function2;)V diff --git a/kotlin-sdk-server/build.gradle.kts b/kotlin-sdk-server/build.gradle.kts index c5feae0..193eb90 100644 --- a/kotlin-sdk-server/build.gradle.kts +++ b/kotlin-sdk-server/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id("mcp.multiplatform") + id("mcp.detekt") id("mcp.publishing") id("mcp.dokka") alias(libs.plugins.kotlinx.binary.compatibility.validator) @@ -12,6 +13,9 @@ kotlin { api(project(":kotlin-sdk-core")) api(libs.ktor.server.core) api(libs.ktor.server.sse) + implementation(libs.ktor.server.content.negotiation) + implementation(libs.ktor.server.websockets) + implementation(libs.ktor.serialization) implementation(libs.kotlin.logging) } } @@ -19,13 +23,24 @@ kotlin { commonTest { dependencies { implementation(kotlin("test")) + implementation(libs.kotest.assertions.core) + implementation(libs.kotest.assertions.json) implementation(libs.kotlinx.coroutines.test) + implementation(project(":test-utils")) } } jvmTest { dependencies { - runtimeOnly(libs.slf4j.simple) + implementation(libs.junit.jupiter.params) + implementation(libs.kotest.assertions.ktor) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.client.logging) + implementation(libs.ktor.serialization) + implementation(libs.ktor.server.content.negotiation) + implementation(libs.ktor.server.test.host) + + implementation(libs.logback.classic) } } } diff --git a/kotlin-sdk-server/detekt-baseline-main.xml b/kotlin-sdk-server/detekt-baseline-main.xml new file mode 100644 index 0000000..aad11fa --- /dev/null +++ b/kotlin-sdk-server/detekt-baseline-main.xml @@ -0,0 +1,7 @@ + + + + + UnsafeCallOnNullableType:StreamableHttpServerTransport.kt:StreamableHttpServerTransport$responseRequestId!! + + diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt new file mode 100644 index 0000000..21e0925 --- /dev/null +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt @@ -0,0 +1,347 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.github.oshai.kotlinlogging.KotlinLogging +import io.modelcontextprotocol.kotlin.sdk.shared.RequestOptions +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageResult +import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequest +import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestFormParams +import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestURLParams +import io.modelcontextprotocol.kotlin.sdk.types.ElicitResult +import io.modelcontextprotocol.kotlin.sdk.types.ElicitationCompleteNotification +import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject +import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult +import io.modelcontextprotocol.kotlin.sdk.types.IncludeContext +import io.modelcontextprotocol.kotlin.sdk.types.ListRootsRequest +import io.modelcontextprotocol.kotlin.sdk.types.ListRootsResult +import io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel +import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.PingRequest +import io.modelcontextprotocol.kotlin.sdk.types.PromptListChangedNotification +import io.modelcontextprotocol.kotlin.sdk.types.Request +import io.modelcontextprotocol.kotlin.sdk.types.RequestId +import io.modelcontextprotocol.kotlin.sdk.types.RequestResult +import io.modelcontextprotocol.kotlin.sdk.types.ResourceListChangedNotification +import io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotification +import io.modelcontextprotocol.kotlin.sdk.types.ServerNotification +import io.modelcontextprotocol.kotlin.sdk.types.ToolListChangedNotification +import io.modelcontextprotocol.kotlin.sdk.types.supportsUrl + +private val logger = KotlinLogging.logger {} + +/** + * Represents a connection interface between a server and a client, enabling + * communication through notifications, requests, and other operations. + * This interface defines various methods to facilitate the interaction. + */ +public interface ClientConnection { + + /** + * A unique identifier identifying the current session. + * Has no inherent meaning. + */ + public val sessionId: String + + /** + * Sends a server-side notification to the client. + * + * @param notification The notification to be sent to the client. This typically represents + * a specific event or update from the server. + * @param relatedRequestId An optional identifier linking this notification to a prior request. + * Useful for correlating notifications with originating requests. + */ + public suspend fun notification(notification: ServerNotification, relatedRequestId: RequestId? = null) + + /** + * Sends a ping request to the client to check connectivity. + * + * @param request Optional request parameters for the ping operation. + * @param options Optional request options, such as timeout or progress callback settings. + * @return The result of the ping request. + * @throws IllegalStateException If for some reason the method is not supported or the connection is closed. + */ + public suspend fun ping(request: PingRequest = PingRequest(), options: RequestOptions? = null): EmptyResult + + /** + * Creates a message using the server's sampling capability. + * + * @param request The parameters for creating a message. + * @param options Optional request options. + * @return The created message result. + * @throws IllegalStateException If the server does not support sampling or if the request fails. + */ + public suspend fun createMessage( + request: CreateMessageRequest, + options: RequestOptions? = null, + ): CreateMessageResult + + /** + * Lists the available "roots" from the client's perspective (if supported). + * + * Roots define the boundaries of where servers can operate within the filesystem, + * allowing them to understand which directories and files they have access to + * + * @param request Optional request parameters containing metadata for the list roots operation. + * @param options Optional request options, such as timeout or progress callback settings. + * @return The list of roots. + * @throws IllegalStateException If the server or client does not support roots. + */ + public suspend fun listRoots( + request: ListRootsRequest = ListRootsRequest(), + options: RequestOptions? = null, + ): ListRootsResult + + /** + * Sends a message to the client requesting elicitation. + * This typically results in a form being displayed to the end user. + * + * Accepted content is validated against [requestedSchema]. If content is absent, an empty + * object is used for validation only, so declared `required` properties still apply. The + * returned [ElicitResult] is not modified. + * + * @param message The message for the elicitation to display. + * @param requestedSchema The schema the server requests for the elicitation result. + * Influences the form displayed to the user. + * @param options Optional request options. + * @return The result of the elicitation request. + * @throws IllegalStateException If the server or client does not support elicitation. + * @throws McpException If an accepted response does not match [requestedSchema]. + */ + public suspend fun createElicitation( + message: String, + requestedSchema: ElicitRequestParams.RequestedSchema, + options: RequestOptions? = null, + ): ElicitResult + + /** + * Sends a URL mode elicitation request to the client, directing the user + * to an external URL for out-of-band interactions. + * + * @param message The message explaining why the interaction is needed. + * @param elicitationId A unique identifier for the elicitation. + * @param url The URL that the user should navigate to. + * @param options Optional request options. + * @return The result of the elicitation request. + * @throws IllegalStateException If the server or client does not support elicitation. + */ + public suspend fun createElicitation( + message: String, + elicitationId: String, + url: String, + options: RequestOptions? = null, + ): ElicitResult + + /** + * Sends a message to the client requesting elicitation. + * This typically results in a form being displayed to the user. + * + * For form-mode requests, accepted content is validated against the requested schema. If + * content is absent, an empty object is used for validation only, so declared `required` + * properties still apply. The returned [ElicitResult] is not modified. + * + * @param request The elicitation request parameters. + * @param options Optional request options. + * @return The result of the elicitation request. + * @throws IllegalStateException If the server or client does not support elicitation. + * @throws McpException If an accepted form-mode response does not match its requested schema. + */ + public suspend fun createElicitation(request: ElicitRequest, options: RequestOptions? = null): ElicitResult + + /** + * Sends a logging message notification to the client. + * Messages are filtered based on the current logging level set by the client. + * If no logging level is set, all messages are sent. + * + * @param notification The logging message notification. + */ + public suspend fun sendLoggingMessage(notification: LoggingMessageNotification) + + /** + * Sends a resource-updated notification to the client, indicating that a specific resource has changed. + * + * @param notification Details of the updated resource. + */ + public suspend fun sendResourceUpdated(notification: ResourceUpdatedNotification) + + /** + * Sends a notification to the client indicating that the list of resources has changed. + */ + public suspend fun sendResourceListChanged() + + /** + * Sends a notification to the client indicating that the list of tools has changed. + */ + public suspend fun sendToolListChanged() + + /** + * Sends a notification to the client indicating that the list of prompts has changed. + */ + public suspend fun sendPromptListChanged() + + /** + * Sends a notification to the client indicating that an out-of-band elicitation has completed. + * + * @param notification Details of the completed elicitation. + */ + public suspend fun sendElicitationComplete(notification: ElicitationCompleteNotification) +} + +internal class ClientConnectionImpl(private val session: ServerSession) : ClientConnection { + + override val sessionId: String get() = session.sessionId + + private suspend fun request(request: Request, options: RequestOptions? = null): T { + logger.trace { "Sending request to client for session $sessionId: $request" } + return session.request(request, options) + } + + override suspend fun notification(notification: ServerNotification, relatedRequestId: RequestId?) { + if (notification is LoggingMessageNotification && !isMessageAccepted(notification.params.level)) { + logger.trace { "Filtering out logging message with level ${notification.params.level}" } + return + } + logger.trace { "Sending notification to client for session $sessionId: $notification" } + session.notification(notification, relatedRequestId) + } + + override suspend fun ping(request: PingRequest, options: RequestOptions?): EmptyResult { + logger.trace { "Sending ping request: $request" } + return request(request, options) + } + + override suspend fun createMessage(request: CreateMessageRequest, options: RequestOptions?): CreateMessageResult { + val caps = session.clientCapabilities + val params = request.params + + if (params.tools != null || params.toolChoice != null) { + requireNotNull(caps?.sampling?.tools) { + "Client did not advertise sampling.tools capability; cannot send " + + "tools/toolChoice in sampling/createMessage request." + } + } + + if (params.includeContext != null && params.includeContext != IncludeContext.None) { + if (caps?.sampling?.context == null) { + logger.warn { + "Client did not advertise sampling.context capability but server requested " + + "includeContext=${params.includeContext}. This is soft-deprecated and may be " + + "rejected by future spec versions." + } + } + } + + validateSamplingMessages(params.messages) + + logger.debug { + "Creating message with ${params.messages.size} messages, maxTokens=${params.maxTokens}, " + + "temperature=${params.temperature}, " + + "systemPrompt=${if (params.systemPrompt != null) "present" else "absent"}, " + + "tools=${params.tools?.size ?: 0}, toolChoice=${params.toolChoice?.mode}" + } + logger.trace { "Full createMessage params: $request" } + return request(request, options) + } + + override suspend fun listRoots(request: ListRootsRequest, options: RequestOptions?): ListRootsResult { + logger.debug { "Listing roots" } + return request(request, options) + } + + override suspend fun createElicitation(request: ElicitRequest, options: RequestOptions?): ElicitResult { + val params = request.params + if (params is ElicitRequestURLParams) { + require(session.clientCapabilities?.elicitation.supportsUrl) { + "Client did not advertise elicitation.url capability; cannot send a URL-mode elicitation." + } + } + logger.debug { + when (params) { + is ElicitRequestFormParams -> + "Creating form elicitation with message length=${params.message.length}, " + + "schema properties count=${params.requestedSchema.properties.size}" + + is ElicitRequestURLParams -> + "Creating URL elicitation with message length=${params.message.length}, " + + "elicitationId=${params.elicitationId}" + } + } + logger.trace { "ElicitRequest: $request" } + val result: ElicitResult = request(request, options) + if (params is ElicitRequestFormParams && result.action == ElicitResult.Action.Accept) { + // Peer-response backstop: accepted form content is client-supplied. Absent content is + // validated as an empty object so declared `required` properties still apply. + validateElicitationContent(params.requestedSchema, result.content ?: EmptyJsonObject) + } + return result + } + + override suspend fun createElicitation( + message: String, + requestedSchema: ElicitRequestParams.RequestedSchema, + options: RequestOptions?, + ): ElicitResult = createElicitation( + request = ElicitRequest(params = ElicitRequestFormParams(message = message, requestedSchema = requestedSchema)), + options = options, + ) + + override suspend fun createElicitation( + message: String, + elicitationId: String, + url: String, + options: RequestOptions?, + ): ElicitResult = createElicitation( + request = ElicitRequest( + params = ElicitRequestURLParams(message = message, elicitationId = elicitationId, url = url), + ), + options = options, + ) + + override suspend fun sendLoggingMessage(notification: LoggingMessageNotification) { + notification(notification) + } + + override suspend fun sendResourceUpdated(notification: ResourceUpdatedNotification) { + logger.debug { "Sending resource updated notification for: ${notification.params.uri}" } + notification(notification) + } + + override suspend fun sendResourceListChanged() { + logger.debug { "Sending resource list changed notification" } + notification(ResourceListChangedNotification()) + } + + override suspend fun sendToolListChanged() { + logger.debug { "Sending tool list changed notification" } + notification(ToolListChangedNotification()) + } + + override suspend fun sendPromptListChanged() { + logger.debug { "Sending prompt list changed notification" } + notification(PromptListChangedNotification()) + } + + override suspend fun sendElicitationComplete(notification: ElicitationCompleteNotification) { + require(session.clientCapabilities?.elicitation.supportsUrl) { + "Client did not advertise elicitation.url capability; cannot send an elicitation completion notification." + } + logger.debug { "Sending elicitation complete notification for: ${notification.params.elicitationId}" } + notification(notification) + } + + /** + * Determines whether a message with the specified logging level is accepted + * based on the current logging level of the session. + * + * @param level The logging level of the message to be evaluated. + * @return True if the message is accepted (i.e., its level meets or exceeds + * the current session's logging level), or if no current logging + * level is set. Otherwise, false. + */ + private fun isMessageAccepted(level: LoggingLevel): Boolean { + val current = session.currentLoggingLevel.value ?: return true // If no level is set, don't filter + + return level >= current + } +} diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ElicitationValidation.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ElicitationValidation.kt new file mode 100644 index 0000000..d976d34 --- /dev/null +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ElicitationValidation.kt @@ -0,0 +1,174 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.modelcontextprotocol.kotlin.sdk.types.BooleanSchema +import io.modelcontextprotocol.kotlin.sdk.types.DoubleSchema +import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.IntegerSchema +import io.modelcontextprotocol.kotlin.sdk.types.LegacyTitledEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.PrimitiveSchemaDefinition +import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import io.modelcontextprotocol.kotlin.sdk.types.StringSchema +import io.modelcontextprotocol.kotlin.sdk.types.TitledMultiSelectEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.TitledSingleSelectEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.UntitledMultiSelectEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.UntitledSingleSelectEnumSchema +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.doubleOrNull +import kotlinx.serialization.json.longOrNull +import kotlin.math.floor + +/** + * Validates accepted form-mode elicitation [content] against the supported constraints in + * [requestedSchema]: types, `required`, numeric bounds, string lengths, enum membership, and item + * counts. Undeclared properties are permitted. + * [StringSchema.format] values are treated as annotations and are not asserted. + * + * @throws McpException with [RPCError.ErrorCode.INVALID_PARAMS] listing the detected violations + * (at most one per property). + */ +internal fun validateElicitationContent(requestedSchema: ElicitRequestParams.RequestedSchema, content: JsonObject) { + val errors = buildList { + for (name in requestedSchema.required.orEmpty()) { + if (name !in content) add("must have required property '$name'") + } + for ((name, schema) in requestedSchema.properties) { + val value = content[name] ?: continue + checkProperty(name, schema, value)?.let(::add) + } + } + if (errors.isNotEmpty()) { + throw McpException( + code = RPCError.ErrorCode.INVALID_PARAMS, + message = "Elicitation response content does not match requested schema: ${errors.joinToString(", ")}", + ) + } +} + +private fun checkProperty(name: String, schema: PrimitiveSchemaDefinition, value: JsonElement): String? = + when (schema) { + is StringSchema -> { + val string = stringValue(value) ?: return "'$name' must be string" + val minLength = schema.minLength + val maxLength = schema.maxLength + val length = codePointCount(string) + when { + minLength != null && length < minLength -> + "'$name' must NOT have fewer than $minLength characters" + + maxLength != null && length > maxLength -> + "'$name' must NOT have more than $maxLength characters" + + else -> null + } + } + + is IntegerSchema -> { + val integer = integerValue(value) ?: return "'$name' must be integer" + checkRange(name, integer, schema.minimum?.toLong(), schema.maximum?.toLong()) + } + + is DoubleSchema -> { + val number = doubleValue(value) ?: return "'$name' must be number" + checkRange(name, number, schema.minimum, schema.maximum) + } + + is BooleanSchema -> if (booleanValue(value) == null) "'$name' must be boolean" else null + + is UntitledSingleSelectEnumSchema -> checkEnum(name, value, schema.enumValues) + + is TitledSingleSelectEnumSchema -> checkEnum(name, value, schema.oneOf.map { it.const }) + + is LegacyTitledEnumSchema -> checkEnum(name, value, schema.enumValues) + + is UntitledMultiSelectEnumSchema -> + checkMultiSelect(name, value, schema.items.enumValues, schema.minItems, schema.maxItems) + + is TitledMultiSelectEnumSchema -> + checkMultiSelect(name, value, schema.items.anyOf.map { it.const }, schema.minItems, schema.maxItems) + } + +// Keep Double statically typed so comparisons use IEEE 754 semantics; generic Comparable ordering +// treats -0.0 as less than 0.0. +private fun checkRange( + name: String, + value: Long, + minimum: Long?, + maximum: Long?, +): String? = when { + minimum != null && value < minimum -> "'$name' must be >= $minimum" + maximum != null && value > maximum -> "'$name' must be <= $maximum" + else -> null +} + +private fun checkRange( + name: String, + value: Double, + minimum: Double?, + maximum: Double?, +): String? = when { + minimum != null && value < minimum -> "'$name' must be >= $minimum" + maximum != null && value > maximum -> "'$name' must be <= $maximum" + else -> null +} + +private fun checkEnum(name: String, value: JsonElement, allowed: List): String? { + val string = stringValue(value) ?: return "'$name' must be string" + return if (string in allowed) null else "'$name' must be equal to one of the allowed values" +} + +private fun checkMultiSelect( + name: String, + value: JsonElement, + allowed: List, + minItems: Int?, + maxItems: Int?, +): String? { + val array = value as? JsonArray ?: return "'$name' must be array" + if (minItems != null && array.size < minItems) return "'$name' must NOT have fewer than $minItems items" + if (maxItems != null && array.size > maxItems) return "'$name' must NOT have more than $maxItems items" + for (element in array) { + val item = stringValue(element) ?: return "'$name' items must be string" + if (item !in allowed) return "'$name' items must be equal to one of the allowed values" + } + return null +} + +// JSON distinguishes the string "42" from the number 42; kotlinx's JsonPrimitive accessors parse +// string content too, so every helper checks isString to preserve that distinction. + +private fun stringValue(value: JsonElement): String? = (value as? JsonPrimitive)?.takeIf { it.isString }?.content + +private fun integerValue(value: JsonElement): Long? { + val primitive = (value as? JsonPrimitive)?.takeIf { !it.isString } ?: return null + primitive.longOrNull?.let { return it } + // JSON Schema's "integer" also matches numbers with a zero fractional part (e.g. 5.0, 1e2). + // Values that do not parse as Long are evaluated using their finite Double representation. + val number = primitive.doubleOrNull ?: return null + return if (number.isFinite() && number == floor(number)) number.toLong() else null +} + +private fun doubleValue(value: JsonElement): Double? = + (value as? JsonPrimitive)?.takeIf { !it.isString }?.doubleOrNull?.takeIf { it.isFinite() } + +private fun booleanValue(value: JsonElement): Boolean? = + (value as? JsonPrimitive)?.takeIf { !it.isString }?.booleanOrNull + +// JSON Schema string lengths count Unicode code points (RFC 8259 characters), not UTF-16 units, +// so a surrogate pair is one character. +private fun codePointCount(string: String): Int { + var count = 0 + var index = 0 + while (index < string.length) { + if (string[index].isHighSurrogate() && index + 1 < string.length && string[index + 1].isLowSurrogate()) { + index++ + } + index++ + count++ + } + return count +} diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/EventStore.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/EventStore.kt new file mode 100644 index 0000000..ff9abbf --- /dev/null +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/EventStore.kt @@ -0,0 +1,33 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage + +/** + * Interface for resumability support via event storage + */ +public interface EventStore { + /** + * Stores an event for later retrieval + * @param streamId ID of the stream the event belongs to + * @param message The JSON-RPC message to store + * @returns The generated event ID for the stored event + */ + public suspend fun storeEvent(streamId: String, message: JSONRPCMessage): String + + /** + * Replays events after the specified event ID + * @param lastEventId The last event ID that was received + * @param sender Function to send events + * @return The stream ID for the replayed events + */ + public suspend fun replayEventsAfter( + lastEventId: String, + sender: suspend (eventId: String, message: JSONRPCMessage) -> Unit, + ): String + + /** + * Returns the stream ID associated with [eventId], or null if the event is unknown. + * Default implementation is a no-op which disables extra validation during replay. + */ + public suspend fun getStreamIdForEventId(eventId: String): String? +} diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Feature.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Feature.kt new file mode 100644 index 0000000..a892c6c --- /dev/null +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Feature.kt @@ -0,0 +1,77 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest +import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult +import io.modelcontextprotocol.kotlin.sdk.types.Prompt +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult +import io.modelcontextprotocol.kotlin.sdk.types.Resource +import io.modelcontextprotocol.kotlin.sdk.types.ResourceTemplate +import io.modelcontextprotocol.kotlin.sdk.types.Tool +import io.modelcontextprotocol.kotlin.sdk.utils.ResourceTemplateMatcher + +internal typealias FeatureKey = String + +/** + * Represents a feature with an associated unique key. + */ +internal interface Feature { + val key: FeatureKey +} + +/** + * A wrapper class representing a registered tool on the server. + * + * @property tool The tool definition. + * @property handler A suspend function to handle the tool call requests. + */ +public data class RegisteredTool( + val tool: Tool, + val handler: suspend ClientConnection.(CallToolRequest) -> CallToolResult, +) : Feature { + override val key: String = tool.name +} + +/** + * A wrapper class representing a registered prompt on the server. + * + * @property prompt The prompt definition. + * @property messageProvider A suspend function that returns the prompt content when requested by the client. + */ +public data class RegisteredPrompt( + val prompt: Prompt, + val messageProvider: suspend ClientConnection.(GetPromptRequest) -> GetPromptResult, +) : Feature { + override val key: String = prompt.name +} + +/** + * A wrapper class representing a registered resource on the server. + * + * @property resource The resource definition. + * @property readHandler A suspend function to handle read requests for this resource. + */ +public data class RegisteredResource( + val resource: Resource, + val readHandler: suspend ClientConnection.(ReadResourceRequest) -> ReadResourceResult, +) : Feature { + override val key: String = resource.uri +} + +/** + * A registered resource template with its associated read handler. + * + * @property resourceTemplate The [ResourceTemplate] definition (RFC 6570 URI template). + * @property matcher Pre-built matcher used to test incoming URIs against [resourceTemplate]. + * @property readHandler A suspend function invoked when a client reads a URI that matches + * this template. The second parameter contains the URI variables extracted from the match. + */ +internal class RegisteredResourceTemplate( + val resourceTemplate: ResourceTemplate, + val matcher: ResourceTemplateMatcher, + val readHandler: suspend ClientConnection.(ReadResourceRequest, Map) -> ReadResourceResult, +) : Feature { + override val key: String = resourceTemplate.uriTemplate +} diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/FeatureNotificationService.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/FeatureNotificationService.kt new file mode 100644 index 0000000..42c2b8f --- /dev/null +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/FeatureNotificationService.kt @@ -0,0 +1,374 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.github.oshai.kotlinlogging.KotlinLogging +import io.modelcontextprotocol.kotlin.sdk.types.Notification +import io.modelcontextprotocol.kotlin.sdk.types.PromptListChangedNotification +import io.modelcontextprotocol.kotlin.sdk.types.ResourceListChangedNotification +import io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotification +import io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotificationParams +import io.modelcontextprotocol.kotlin.sdk.types.ServerNotification +import io.modelcontextprotocol.kotlin.sdk.types.ToolListChangedNotification +import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.getAndUpdate +import kotlinx.collections.immutable.persistentMapOf +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.takeWhile +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlin.time.Clock +import kotlin.time.ExperimentalTime + +/** + * Represents an event for the notification service. + * + * @property timestamp A timestamp for the event. + */ +private sealed class NotificationEvent(open val timestamp: Long) + +/** + * Represents an event for a notification. + * + * @property timestamp Timestamp at which this event was created. + * @property notification The notification associated with the event. + */ +private class SendEvent(override val timestamp: Long, val notification: Notification) : NotificationEvent(timestamp) + +/** Represents an event marking the end of notification processing. */ +private class EndEvent(override val timestamp: Long) : NotificationEvent(timestamp) + +/** + * Represents a job that handles session-specific notifications, processing events + * and delivering relevant notifications to the associated session. + * + * This class listens to a stream of notification events and processes them + * based on the event type and the resource subscriptions associated with the session. + * It allows subscribing to or unsubscribing from specific resource keys for granular + * notification handling. The job can also be canceled to stop processing further events. + * Notification with timestamps older than the starting timestamp are skipped. + */ +private class SessionNotificationJob { + private val job: Job + private val resourceSubscriptions = atomic(persistentMapOf()) + private val logger = KotlinLogging.logger {} + + /** + * Constructor for the SessionNotificationJob, responsible for processing notification events + * and dispatching appropriate notifications to the provided server session. The job operates + * within the given coroutine scope and begins handling events starting from the specified + * timestamp. + * + * @param session The server session where notifications will be dispatched. + * @param scope The coroutine scope in which this job operates. + * @param events A shared flow of notification events that the job listens to. + * @param fromTimestamp The timestamp from which the job starts processing events. + */ + constructor( + session: ServerSession, + scope: CoroutineScope, + events: SharedFlow, + fromTimestamp: Long, + ) { + logger.info { "Starting notification job from timestamp $fromTimestamp for sessionId: ${session.sessionId} " } + job = scope.launch { + events.takeWhile { it !is EndEvent }.collect { event -> + when (event) { + is SendEvent -> { + handleSendNotificationEvent(event, session, fromTimestamp) + } + + else -> { + logger.warn { "Skipping event: $event" } + } + } + } + } + } + + /** + * Handles sending a notification event to a specific server session. + * + * @param event The notification event to be processed. + * @param session The server session to which the notification should be sent. + * @param fromTimestamp The timestamp to filter events. + * Notifications with timestamps older than this value are skipped. + */ + private suspend fun handleSendNotificationEvent(event: SendEvent, session: ServerSession, fromTimestamp: Long) { + if (event.timestamp < fromTimestamp) { + logger.info { + "Skipping event with id: ${event.timestamp} as it is older than startingEventId $fromTimestamp: $event" + } + return + } + when (val notification = event.notification) { + is PromptListChangedNotification, + is ResourceListChangedNotification, + is ToolListChangedNotification, + -> { + logger.info { "Sending list changed notification for sessionId: ${session.sessionId}" } + session.notification(notification) + } + + is ResourceUpdatedNotification -> { + resourceSubscriptions.value[notification.params.uri]?.let { resourceFromTimestamp -> + if (event.timestamp >= resourceFromTimestamp) { + logger.info { + "Sending resource updated notification for resource ${notification.params.uri} " + + "to sessionId: ${session.sessionId}" + } + session.notification(notification) + } else { + logger.info { + "Skipping resource updated notification for resource ${notification.params.uri} " + + "as it is older than subscription timestamp $resourceFromTimestamp" + } + } + } ?: { + logger.info { + "No subscription for resource ${notification.params.uri}. " + + "Skipping notification: $notification" + } + } + } + + else -> { + logger.warn { "Skipping notification: $notification" } + } + } + } + + /** + * Subscribes to a resource identified by the given feature key. + * + * @param resourceKey The key representing the resource to subscribe to. + * @param timestamp The timestamp of the subscription. + */ + fun subscribe(resourceKey: FeatureKey, timestamp: Long) { + resourceSubscriptions.getAndUpdate { it.put(resourceKey, timestamp) } + } + + /** + * Unsubscribes from a resource identified by the given feature key. + * + * @param resourceKey The key representing the resource to unsubscribe from. + */ + fun unsubscribe(resourceKey: FeatureKey) { + resourceSubscriptions.getAndUpdate { it.remove(resourceKey) } + } + + /** + * Waits for the notification service to complete its operations. + */ + suspend fun join() { + job.join() + } + + /** + * Cancels the notification service job. + */ + fun cancel() { + job.cancel() + } +} + +/** + * Service responsible for managing and emitting notifications related to feature changes. + * + * This service facilitates notification subscriptions for different sessions and supports managing + * listeners for feature-related events. Notifications include changes in tool lists, prompt lists, + * resource lists, and updates to specific resources. + * + * This class operates on a background coroutine scope to handle notifications asynchronously. + * It maintains jobs associated with sessions and features for controlling active subscriptions. + * + * Key Responsibilities: + * - Emit notifications for various feature-related events. + * - Provide listeners for handling feature change events. + * - Allow clients to subscribe or unsubscribe from specific notifications. + * + * Notifications managed: + * - Tool list change notifications. + * - Prompt list change notifications. + * - Resource list change notifications. + * - Resource updates pertaining to specific resources. + */ +internal class FeatureNotificationService( + notificationBufferCapacity: Int = Channel.UNLIMITED, + @OptIn(ExperimentalTime::class) + private val clock: Clock = Clock.System, +) { + /** Coroutine scope used to handle asynchronous notifications. */ + private val notificationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + + /** Shared flow used to emit events within the feature notification service. */ + private val notificationEvents = MutableSharedFlow( + extraBufferCapacity = notificationBufferCapacity, + onBufferOverflow = BufferOverflow.SUSPEND, + ) + + /** Active emit jobs. */ + private val emitJobs = atomic(persistentSetOf()) + + /** Notification jobs associated with sessions. */ + private val sessionNotificationJobs = atomic(persistentMapOf()) + + private val logger = KotlinLogging.logger {} + + private fun featureListener(notificationProvider: (FeatureKey) -> ServerNotification): FeatureListener = + object : FeatureListener { + override fun onFeatureUpdated(featureKey: FeatureKey) { + val notification = notificationProvider(featureKey) + logger.debug { "Emitting notification: ${notification.method.value}" } + emit(notification) + } + } + + /** Listener for tool feature events. */ + internal val toolListChangedListener: FeatureListener = featureListener { ToolListChangedNotification() } + + /** Listener for prompt feature events. */ + internal val promptListChangedListener: FeatureListener = featureListener { PromptListChangedNotification() } + + /** Listener for resource feature events. */ + internal val resourceListChangedListener: FeatureListener = featureListener { ResourceListChangedNotification() } + + /** Listener for resource update events. */ + internal val resourceUpdatedListener: FeatureListener = + featureListener { ResourceUpdatedNotification(ResourceUpdatedNotificationParams(uri = it)) } + + /** + * Subscribes session to list changed notifications for all features and resource update notifications. + * For each session the job is created and stored until the [unsubscribeSession] method is called. + * In case of session already subscribed to list changed notifications, the method will skip the subscription and + * continue to send notification using the existing job. + * + * @param session The session to subscribe. + */ + internal fun subscribeSession(session: ServerSession) { + logger.info { "Subscribing session for notifications sessionId: ${session.sessionId}" } + + val timestamp = getCurrentTimestamp() + + sessionNotificationJobs.getAndUpdate { + if (it.containsKey(session.sessionId)) { + logger.info { "Session already subscribed: ${session.sessionId}" } + return@getAndUpdate it + } else { + it.put( + session.sessionId, + SessionNotificationJob( + session = session, + scope = notificationScope, + events = notificationEvents, + // Save the first event id to process, as notification can be emitted after the subscription + fromTimestamp = timestamp, + ), + ) + } + } + + logger.info { "Subscribed session for notifications sessionId: ${session.sessionId}" } + } + + /** + * Unsubscribes a session from list changed notifications for all features. + * Cancels and removes the job associated with the given session's notifications. + * + * @param session The session to unsubscribe from list changed notifications. + */ + internal fun unsubscribeSession(session: ServerSession) { + logger.info { "Unsubscribing from list changed notifications for sessionId: ${session.sessionId}" } + sessionNotificationJobs.getAndUpdate { + it[session.sessionId]?.cancel() + it.remove(session.sessionId) + } + logger.info { "Unsubscribed from list changed notifications for sessionId: ${session.sessionId}" } + } + + /** + * Subscribes a session to notifications for resource updates pertaining to the given resource key. + * + * @param session The session to subscribe. + * @param resourceKey The resource key to subscribe to. + */ + internal fun subscribeToResourceUpdate(session: ServerSession, resourceKey: FeatureKey) { + logger.info { "Subscribing to resource $resourceKey update notifications for sessionId: ${session.sessionId}" } + // Set starting event id for resources notifications to skip events emitted before the subscription + sessionNotificationJobs.value[session.sessionId]?.subscribe(resourceKey, getCurrentTimestamp()) + logger.info { "Subscribed to resource $resourceKey update notifications for sessionId: ${session.sessionId}" } + } + + /** + * Unsubscribes a session from notifications for resource updates pertaining to the given resource key. + * + * @param session The session to unsubscribe from. + * @param resourceKey The resource key to unsubscribe from. + */ + internal fun unsubscribeFromResourceUpdate(session: ServerSession, resourceKey: FeatureKey) { + logger.info { + "Unsubscribing from resource $resourceKey update notifications for sessionId: ${session.sessionId}" + } + sessionNotificationJobs.value[session.sessionId]?.unsubscribe(resourceKey) + logger.info { + "Unsubscribed from resource $resourceKey update notifications for sessionId: ${session.sessionId}" + } + } + + /** Emits a notification to all active sessions. */ + private fun emit(notification: Notification) { + // Create a timestamp before emit to ensure notifications are processed in order + val timestamp = getCurrentTimestamp() + + logger.info { "Emitting notification $timestamp: $notification" } + + // Launching emit lazily to put it to the jobs queue before the completion + val job = notificationScope.launch(start = CoroutineStart.LAZY) { + logger.debug { "Actually emitting notification $timestamp: $notification" } + notificationEvents.emit(SendEvent(timestamp, notification)) + logger.debug { "Notification emitted $timestamp: $notification" } + } + + // Add job to set before starting + emitJobs.getAndUpdate { it.add(job) } + + // Register completion + job.invokeOnCompletion { _ -> + emitJobs.getAndUpdate { it.remove(job) } + } + + // Start the job after it's safely added + job.start() + } + + /** Returns the current timestamp in milliseconds since Unix epoch. */ + @OptIn(ExperimentalTime::class) + private fun getCurrentTimestamp(): Long = clock.now().toEpochMilliseconds() + + suspend fun close() { + logger.info { "Closing feature notification service" } + + // Making sure all emitting jobs are completed + emitJobs.value.joinAll() + + // Emitting end event to complete all session notification jobs + notificationScope.launch { + logger.info { "Emitting end event" } + notificationEvents.emit(EndEvent(getCurrentTimestamp())) + logger.info { "End event emitted" } + }.join() + + // Making sure all session notification jobs are completed (after receiving the end event) + sessionNotificationJobs.value.values.forEach { it.join() } + // Cancelling notification scope to stop processing further events + notificationScope.cancel() + } +} diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/FeatureRegistry.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/FeatureRegistry.kt new file mode 100644 index 0000000..142d59c --- /dev/null +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/FeatureRegistry.kt @@ -0,0 +1,178 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.github.oshai.kotlinlogging.KotlinLogging +import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.getAndUpdate +import kotlinx.atomicfu.update +import kotlinx.collections.immutable.minus +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentMapOf +import kotlinx.collections.immutable.toPersistentSet + +/** + * A listener interface for receiving notifications about feature changes in registry. + */ +internal interface FeatureListener { + fun onFeatureUpdated(featureKey: String) +} + +/** + * A generic registry for managing features of a specified type. This class provides thread-safe + * operations for adding, removing, and retrieving features from the registry. + * + * Feature keys are unique: [add] and [addAll] reject keys that are already registered. + * To replace a feature, [remove] it first and add the new one. + * + * @param T The type of the feature, constrained to implement the [Feature] interface. + * @param featureType A string description of the type of feature being managed. + * Used for logging and error messages. + */ +internal class FeatureRegistry(private val featureType: String) { + + private val logger = KotlinLogging.logger(name = "FeatureRegistry[$featureType]") + + /** + * Atomic variable used to maintain a thread-safe registry of features. + * Stores a persistent map where each feature is identified by a unique key. + */ + private val registry = atomic(persistentMapOf()) + + /** + * Provides a snapshot of all features currently registered in the registry. + * Keys represent unique identifiers for each feature, and values represent the associated features. + */ + internal val values: Map + get() = registry.value + + private val listeners = atomic(persistentListOf()) + + internal fun addListener(listener: FeatureListener) { + listeners.update { it.adding(listener) } + } + + internal fun removeListener(listener: FeatureListener) { + listeners.update { it.removing(listener) } + } + + /** + * Adds the specified feature to the registry. + * + * @param feature The feature to be added to the registry. + */ + internal fun add(feature: T) { + logger.info { "Adding $featureType: \"${feature.key}\"" } + registry.update { current -> + require(!current.containsKey(feature.key)) { + "$featureType \"${feature.key}\" is already registered. Remove it first to replace it." + } + current.putting(feature.key, feature) + } + + logger.info { "Added $featureType: \"${feature.key}\"" } + notifyFeatureUpdated(null, feature) + } + + /** + * Adds the given list of features to the registry. Each feature is mapped by its key + * and added to the current registry. The operation is all-or-nothing: if any key is + * rejected, no features are added. + * + * @param features The list of features to add to the registry. + */ + internal fun addAll(features: List) { + logger.info { "Adding ${featureType}s: ${features.size}" } + val newEntries = features.associateBy { it.key } + require(newEntries.size == features.size) { + val duplicated = features.groupingBy { it.key }.eachCount().filterValues { it > 1 }.keys + "Duplicate $featureType keys in batch: $duplicated" + } + registry.update { current -> + val conflicting = newEntries.keys.filter { current.containsKey(it) } + require(conflicting.isEmpty()) { + "${featureType}s already registered: $conflicting. Remove them first to replace them." + } + current.puttingAll(newEntries) + } + + logger.info { "Added ${featureType}s: ${features.size}" } + for (feature in features) { + notifyFeatureUpdated(null, feature) + } + } + + /** + * Removes the feature associated with the given key from the registry. + * + * @param key The key of the feature to be removed. + * @return `true` if the feature was successfully removed, or `false` if the feature was not found. + */ + internal fun remove(key: FeatureKey): Boolean { + logger.info { "Removing $featureType: \"$key\"" } + val oldMap = registry.getAndUpdate { current -> current.removing(key) } + + val removedFeature = oldMap[key] + val removed = removedFeature != null + + if (removed) { + logger.info { "Removed $featureType: \"$key\"" } + notifyFeatureUpdated(removedFeature, null) + } else { + logger.info { "$featureType not found: \"$key\"" } + } + + return removed + } + + /** + * Removes the features associated with the given keys from the registry. + * + * @param keys The list of keys whose associated features are to be removed. + * @return The number of features that were successfully removed. + */ + internal fun removeAll(keys: List): Int { + logger.info { "Removing ${featureType}s: ${keys.size}" } + val oldMap = registry.getAndUpdate { current -> current - keys.toPersistentSet() } + + val removedFeatures = keys.mapNotNull { oldMap[it] } + val removedCount = removedFeatures.size + + if (removedCount > 0) { + logger.info { "Removed ${featureType}s: $removedCount" } + } else { + logger.info { "No $featureType were removed" } + } + removedFeatures.forEach { + notifyFeatureUpdated(it, null) + } + + return removedCount + } + + /** + * Retrieves the feature associated with the given key from the registry. + * + * @param key The key of the feature to retrieve. + * @return The feature associated with the given key, or `null` if no such feature exists in the registry. + */ + internal fun get(key: FeatureKey): T? { + logger.info { "Getting $featureType: \"$key\"" } + val feature = registry.value[key] + if (feature != null) { + logger.info { "Got $featureType: \"$key\"" } + } else { + logger.info { "$featureType not found: \"$key\"" } + } + + return feature + } + + private fun notifyFeatureUpdated(oldFeature: T?, newFeature: T?) { + val featureKey = (oldFeature?.key ?: newFeature?.key) ?: run { + logger.error { "Notification should have feature key, but none found" } + return + } + + logger.info { "Notifying listeners on feature update" } + listeners.value.forEach { it.onFeatureUpdated(featureKey) } + } +} diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/HostValidation.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/HostValidation.kt new file mode 100644 index 0000000..ef010fe --- /dev/null +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/HostValidation.kt @@ -0,0 +1,195 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.parseUrl +import io.ktor.server.application.ApplicationCall +import io.ktor.server.application.RouteScopedPlugin +import io.ktor.server.application.createRouteScopedPlugin +import io.ktor.server.request.header +import io.ktor.server.response.respondText +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCError +import io.modelcontextprotocol.kotlin.sdk.types.McpJson +import io.modelcontextprotocol.kotlin.sdk.types.RPCError + +/** + * Default list of hostnames allowed for localhost DNS rebinding protection. + * Matches the TypeScript SDK's `localhostAllowedHostnames()`. + */ +internal val LOCALHOST_ALLOWED_HOSTS: List = listOf("localhost", "127.0.0.1", "[::1]") + +/** + * Default list of `Origin` values allowed for localhost DNS rebinding protection. + * + * Mirrors [LOCALHOST_ALLOWED_HOSTS] but carries a scheme so the values parse as URLs: + * [extractOriginHost] rejects schemeless input. Comparison is hostname-only and + * scheme-agnostic, so these entries also match `https://` origins on the same host. + */ +internal val LOCALHOST_ALLOWED_ORIGINS: List = + listOf("http://localhost", "http://127.0.0.1", "http://[::1]") + +/** + * Characters that are valid in a URL but must not appear in an HTTP `Host` header. + * Rejecting them prevents the parser from accepting malformed values + * (e.g. `evil.com@localhost`, `host/path`) that a generic URL parser would silently allow. + */ +private val FORBIDDEN_HOST_CHARS: CharArray = charArrayOf('/', '@', '?', '#') + +/** + * Extracts the hostname from a Host header value, stripping the port. + * + * Only accepts the strict `host [ ":" port ]` / `"[" ipv6 "]" [ ":" port ]` + * format defined by RFC 7230. Values containing URL-only characters + * (`/`, `@`, `?`, `#`) or whitespace are rejected. + * + * Examples: + * - `"localhost:3000"` → `"localhost"` + * - `"127.0.0.1:8080"` → `"127.0.0.1"` + * - `"[::1]:3000"` → `"[::1]"` + * - `"example.com"` → `"example.com"` + * - `"evil.com@localhost"` → `null` + * + * @return the hostname, or `null` if the value is blank, malformed, or contains forbidden characters. + */ +internal fun extractHostname(hostHeader: String): String? = when { + hostHeader.isBlank() -> null + + hostHeader.any { it in FORBIDDEN_HOST_CHARS || it.isWhitespace() } -> null + + hostHeader.startsWith("[") -> { + val end = hostHeader.indexOf(']') + when { + end <= 1 -> null + isValidPortSuffix(hostHeader.substring(end + 1)) -> hostHeader.substring(0, end + 1) + else -> null + } + } + + else -> { + val colon = hostHeader.indexOf(':') + when { + colon < 0 -> hostHeader + colon == 0 -> null + isValidPortSuffix(hostHeader.substring(colon)) -> hostHeader.substring(0, colon) + else -> null + } + } +} + +/** + * Checks that [tail] is either empty (no port part) or `":"` followed by zero or more digits, + * matching RFC 7230 §3.2.3 where `port = *DIGIT`. Non-empty non-digit characters are rejected. + */ +private fun isValidPortSuffix(tail: String): Boolean = when { + tail.isEmpty() -> true + tail[0] != ':' -> false + else -> tail.substring(1).all { it.isDigit() } +} + +/** + * Extracts the hostname from an `Origin` header value. + * + * Uses Ktor's [parseUrl] to avoid reimplementing RFC 6454 parsing. + * Scheme, port, userinfo, and path are discarded — only the host matters for + * DNS-rebinding protection. The result is **not** normalized to lower-case; + * callers should apply [String.lowercase] if they need case-insensitive comparison. + * + * @return the hostname, or `null` if [origin] cannot be parsed or has no host part. + */ +internal fun extractOriginHost(origin: String): String? = parseUrl(origin)?.host?.takeIf(String::isNotEmpty) + +/** + * Configuration for the [DnsRebindingProtection] Ktor route-scoped plugin. + * + * @property allowedHosts List of hostnames allowed in the `Host` header. + * Comparison is port-agnostic and case-insensitive. + * Defaults to `localhost`, `127.0.0.1`, `[::1]`. + * An empty list will reject **all** requests. + * @property allowedOrigins Optional list of allowed `Origin` values. Entries are parsed as URLs + * (via [parseUrl]) and compared by **hostname only** — scheme and port are ignored. + * If `null`, origin validation is disabled. + * If configured, requests **with** an `Origin` header whose hostname is not in the list + * are rejected, but requests **without** an `Origin` header are allowed (non-browser clients). + */ +public class DnsRebindingProtectionConfig { + public var allowedHosts: List = LOCALHOST_ALLOWED_HOSTS + public var allowedOrigins: List? = null +} + +/** + * Ktor route-scoped plugin that validates `Host` and `Origin` headers + * to protect against DNS rebinding attacks. + * + * Install on a route to intercept all requests **before** handlers: + * ```kotlin + * route("/mcp") { + * install(DnsRebindingProtection) { + * allowedHosts = listOf("myapp.com", "localhost") + * } + * // handlers... + * } + * ``` + */ +public val DnsRebindingProtection: RouteScopedPlugin = + createRouteScopedPlugin( + "MCP-DnsRebindingProtection", + ::DnsRebindingProtectionConfig, + ) { + val hosts: Set = pluginConfig.allowedHosts.mapTo(mutableSetOf()) { + extractHostname(it)?.lowercase() + ?: error("Invalid host in DnsRebindingProtection.allowedHosts: '$it'") + } + val originHosts: Set? = pluginConfig.allowedOrigins?.mapTo(mutableSetOf()) { + extractOriginHost(it)?.lowercase() + ?: error("Invalid origin in DnsRebindingProtection.allowedOrigins: '$it'") + } + + onCall { call -> + val hostHeader = call.request.header(HttpHeaders.Host) + val hostname = hostHeader?.let { extractHostname(it) }?.lowercase() + + if (hostname == null) { + call.rejectDnsValidation("Invalid Host header: (malformed or missing)") + return@onCall + } + if (hostname !in hosts) { + call.rejectDnsValidation("Invalid Host: $hostname") + return@onCall + } + + if (originHosts != null) { + val originHeader = call.request.header(HttpHeaders.Origin) + // Allow requests without Origin (non-browser clients cannot perform DNS rebinding) + if (originHeader != null) { + val originHost = extractOriginHost(originHeader)?.lowercase() + if (originHost == null) { + call.rejectDnsValidation("Invalid Origin header: (unparseable)") + return@onCall + } + if (originHost !in originHosts) { + call.rejectDnsValidation("Invalid Origin host: $originHost") + return@onCall + } + } + } + } + } + +/** + * Responds with a 403 Forbidden JSON-RPC error without requiring ContentNegotiation. + */ +private suspend fun ApplicationCall.rejectDnsValidation(message: String) { + val error = JSONRPCError( + id = null, + error = RPCError( + code = -32_000, + message = message, + ), + ) + respondText( + McpJson.encodeToString(error), + ContentType.Application.Json, + HttpStatusCode.Forbidden, + ) +} diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt index 57bae05..3127499 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt @@ -1,109 +1,434 @@ package io.modelcontextprotocol.kotlin.sdk.server import io.github.oshai.kotlinlogging.KotlinLogging +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpMethod import io.ktor.http.HttpStatusCode import io.ktor.server.application.Application +import io.ktor.server.application.ApplicationCall +import io.ktor.server.application.ApplicationCallPipeline +import io.ktor.server.application.MissingApplicationPluginException import io.ktor.server.application.install +import io.ktor.server.request.ApplicationRequest +import io.ktor.server.request.header +import io.ktor.server.request.httpMethod +import io.ktor.server.response.header import io.ktor.server.response.respond -import io.ktor.server.routing.Routing +import io.ktor.server.routing.Route import io.ktor.server.routing.RoutingContext +import io.ktor.server.routing.delete +import io.ktor.server.routing.get import io.ktor.server.routing.post import io.ktor.server.routing.route import io.ktor.server.routing.routing +import io.ktor.server.sse.Heartbeat import io.ktor.server.sse.SSE import io.ktor.server.sse.ServerSSESession +import io.ktor.server.sse.heartbeat import io.ktor.server.sse.sse -import io.ktor.util.collections.ConcurrentMap import io.ktor.utils.io.KtorDsl +import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import kotlinx.coroutines.awaitCancellation private val logger = KotlinLogging.logger {} +/** + * Registers MCP over [Server-Sent Events (SSE) Transport](https://modelcontextprotocol.io/specification/2024-11-05/basic/transports#http-with-sse) + * at the specified [path] on this [Route]. + * + * **Precondition:** the [SSE] plugin must be installed on the application before calling this function. + * Use [Application.mcp] if you want SSE to be installed automatically. + * + * @param path the URL path to register the SSE endpoint. + * @param enableDnsRebindingProtection whether to install [DnsRebindingProtection] on this route. Defaults to `true`. + * @param allowedHosts hostnames allowed in the `Host` header. Defaults to `localhost`, `127.0.0.1`, `[::1]`. + * @param allowedOrigins origins allowed in the `Origin` header, compared by hostname only + * (scheme and port are ignored). Requests without an `Origin` header are allowed. + * When `null` while the localhost host defaults are in effect (no custom `allowedHosts`), + * the `Origin` header is validated against `localhost`, `127.0.0.1`, `[::1]`. + * With custom `allowedHosts`, `null` skips origin validation. + * @param maxRequestBodySize maximum allowed size, in bytes, of an incoming POST body; larger requests are + * rejected with `413 Payload Too Large`. Defaults to 4 MiB. + * @param block factory block with access to the [ServerSSESession] + * that creates and returns the [Server] to handle the connection. + * @throws IllegalStateException if the [SSE] plugin is not installed. + */ @KtorDsl -public fun Routing.mcp(path: String, block: ServerSSESession.() -> Server) { +@Suppress("LongParameterList") +public fun Route.mcp( + path: String, + enableDnsRebindingProtection: Boolean = true, + allowedHosts: List? = null, + allowedOrigins: List? = null, + maxRequestBodySize: Long = DEFAULT_MAX_REQUEST_BODY_SIZE, + block: ServerSSESession.() -> Server, +) { route(path) { - mcp(block) + mcp(enableDnsRebindingProtection, allowedHosts, allowedOrigins, maxRequestBodySize, block) } } /** - * Configures the Ktor Application to handle Model Context Protocol (MCP) over Server-Sent Events (SSE). + * Registers MCP over [Server-Sent Events (SSE) Transport](https://modelcontextprotocol.io/specification/2024-11-05/basic/transports#http-with-sse) + * endpoints on this [Route]. + * + * **Precondition:** the [SSE] plugin must be installed on the application before calling this function. + * Use [Application.mcp] if you want SSE to be installed automatically. + * + * @param enableDnsRebindingProtection whether to install [DnsRebindingProtection] on this route. Defaults to `true`. + * @param allowedHosts hostnames allowed in the `Host` header. Defaults to `localhost`, `127.0.0.1`, `[::1]`. + * @param allowedOrigins origins allowed in the `Origin` header, compared by hostname only + * (scheme and port are ignored). Requests without an `Origin` header are allowed. + * When `null` while the localhost host defaults are in effect (no custom `allowedHosts`), + * the `Origin` header is validated against `localhost`, `127.0.0.1`, `[::1]`. + * With custom `allowedHosts`, `null` skips origin validation. + * @param maxRequestBodySize maximum allowed size, in bytes, of an incoming POST body; larger requests are + * rejected with `413 Payload Too Large`. Defaults to 4 MiB. + * @param block factory block with access to the [ServerSSESession] + * that creates and returns the [Server] to handle the connection. + * @throws IllegalStateException if the [SSE] plugin is not installed. */ @KtorDsl -public fun Routing.mcp(block: ServerSSESession.() -> Server) { - val transports = ConcurrentMap() +@Suppress("LongParameterList") +public fun Route.mcp( + enableDnsRebindingProtection: Boolean = true, + allowedHosts: List? = null, + allowedOrigins: List? = null, + maxRequestBodySize: Long = DEFAULT_MAX_REQUEST_BODY_SIZE, + block: ServerSSESession.() -> Server, +) { + try { + plugin(SSE) + } catch (e: MissingApplicationPluginException) { + throw IllegalStateException( + "The SSE plugin must be installed before registering MCP routes. " + + "Add `install(SSE)` to your application configuration, " + + "or use Application.mcp() which installs it automatically.", + e, + ) + } + + installDnsRebindingProtection(enableDnsRebindingProtection, allowedHosts, allowedOrigins) + + val transportManager = TransportManager() sse { - mcpSseEndpoint("", transports, block) + mcpSseEndpoint("", transportManager, maxRequestBodySize, block) } post { - mcpPostEndpoint(transports) + mcpPostEndpoint(transportManager) } } -@Suppress("FunctionName") -@Deprecated("Use mcp() instead", ReplaceWith("mcp(block)"), DeprecationLevel.WARNING) -public fun Application.MCP(block: ServerSSESession.() -> Server) { - mcp(block) -} - +/** + * Configures the Ktor Application to handle Model Context Protocol (MCP) + * over [Server-Sent Events (SSE) Transport](https://modelcontextprotocol.io/specification/2024-11-05/basic/transports#http-with-sse) + * and sets up routing with the provided configuration block. + * + * Automatically installs [ContentNegotiation][io.ktor.server.plugins.contentnegotiation.ContentNegotiation] + * with [McpJson][io.modelcontextprotocol.kotlin.sdk.types.McpJson] and [SSE]. + * + * @param enableDnsRebindingProtection whether to install [DnsRebindingProtection] on this route. Defaults to `true`. + * @param allowedHosts hostnames allowed in the `Host` header. Defaults to `localhost`, `127.0.0.1`, `[::1]`. + * @param allowedOrigins origins allowed in the `Origin` header, compared by hostname only + * (scheme and port are ignored). Requests without an `Origin` header are allowed. + * When `null` while the localhost host defaults are in effect (no custom `allowedHosts`), + * the `Origin` header is validated against `localhost`, `127.0.0.1`, `[::1]`. + * With custom `allowedHosts`, `null` skips origin validation. + * @param maxRequestBodySize maximum allowed size, in bytes, of an incoming POST body; larger requests are + * rejected with `413 Payload Too Large`. Defaults to 4 MiB. + * @param block factory block with access to the [ServerSSESession] + * that creates and returns the [Server] to handle the connection. + */ @KtorDsl -public fun Application.mcp(block: ServerSSESession.() -> Server) { - val transports = ConcurrentMap() - +@Suppress("LongParameterList") +public fun Application.mcp( + enableDnsRebindingProtection: Boolean = true, + allowedHosts: List? = null, + allowedOrigins: List? = null, + maxRequestBodySize: Long = DEFAULT_MAX_REQUEST_BODY_SIZE, + block: ServerSSESession.() -> Server, +) { + installMcpContentNegotiation() install(SSE) routing { - sse("/sse") { - mcpSseEndpoint("/message", transports, block) - } + mcp(enableDnsRebindingProtection, allowedHosts, allowedOrigins, maxRequestBodySize, block) + } +} - post("/message") { - mcpPostEndpoint(transports) +@Suppress("LongParameterList") +private fun Application.mcpStreamableHttp( + path: String = "/mcp", + enableDnsRebindingProtection: Boolean, + allowedHosts: List?, + allowedOrigins: List?, + configuration: StreamableHttpServerTransport.Configuration, + sseHeartbeatConfig: (Heartbeat.() -> Unit)?, + block: RoutingContext.() -> Server, +) { + installMcpContentNegotiation() + install(SSE) + + val transportManager = TransportManager() + + routing { + route(path) { + installDnsRebindingProtection(enableDnsRebindingProtection, allowedHosts, allowedOrigins) + + // Set Mcp-Session-Id on GET responses before Ktor's sse {} commits headers. + intercept(ApplicationCallPipeline.Plugins) { + if (context.request.httpMethod == HttpMethod.Get) { + val sessionId = context.request.header(MCP_SESSION_ID_HEADER) + if (sessionId != null && transportManager.getTransport(sessionId) != null) { + context.response.header(MCP_SESSION_ID_HEADER, sessionId) + } + } + } + + sse { + val transport = existingStreamableTransport(call, transportManager) ?: return@sse + sseHeartbeatConfig?.let { config -> heartbeat(config) } + transport.handleRequest(this, call) + } + + post { + val transport = streamableTransport( + transportManager = transportManager, + configuration = configuration, + block = block, + ) ?: return@post + + transport.handleRequest(null, call) + } + + delete { + val transport = existingStreamableTransport(call, transportManager) ?: return@delete + transport.handleRequest(null, call) + } } } } +/** + * Configures the Ktor Application to handle Model Context Protocol (MCP) + * over [Streamable HTTP Transport](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http) + * + * Sets up SSE, HTTP POST, and DELETE endpoints at the specified [path]. + * Simple request/response pairs are returned as JSON (not SSE streams). + * + * Automatically installs [ContentNegotiation][io.ktor.server.plugins.contentnegotiation.ContentNegotiation] + * with [McpJson][io.modelcontextprotocol.kotlin.sdk.types.McpJson] and [SSE]. + * + * @param path The base path for the MCP Streamable HTTP endpoint. Defaults to "/mcp". + * @param enableDnsRebindingProtection Enables DNS rebinding attack protection for the endpoint. Defaults to `true`. + * @param allowedHosts A list of hostnames allowed to access the endpoint. + * If `null` and DNS rebinding protection is enabled, defaults to `localhost`, `127.0.0.1`, `[::1]`. + * @param allowedOrigins A list of allowed `Origin` header values, compared by hostname only + * (scheme and port are ignored). Requests without an `Origin` header are allowed. + * When `null` while the localhost host defaults are in effect (no custom `allowedHosts`), + * the `Origin` header is validated against `localhost`, `127.0.0.1`, `[::1]`. + * With custom `allowedHosts`, `null` skips origin validation. + * @param eventStore An optional [EventStore] instance to enable resumable event stream functionality. + * Allows storing and replaying events. + * @param sseHeartbeatConfig The heartbeat configuration option for SSE connections. `null` means no heartbeat is sent. + * @param block factory block with access to the [RoutingContext] (for reading request headers) + * that creates and returns the [Server] to handle the connection. + */ +@KtorDsl +public fun Application.mcpStreamableHttp( + path: String = "/mcp", + enableDnsRebindingProtection: Boolean = true, + allowedHosts: List? = null, + allowedOrigins: List? = null, + eventStore: EventStore? = null, + sseHeartbeatConfig: (Heartbeat.() -> Unit)? = null, + block: RoutingContext.() -> Server, +) { + mcpStreamableHttp( + path = path, + enableDnsRebindingProtection = enableDnsRebindingProtection, + allowedHosts = allowedHosts, + allowedOrigins = allowedOrigins, + configuration = StreamableHttpServerTransport.Configuration( + eventStore = eventStore, + enableJsonResponse = true, + ), + sseHeartbeatConfig = sseHeartbeatConfig, + block = block, + ) +} + +@Suppress("LongParameterList") +private fun Application.mcpStatelessStreamableHttp( + path: String = "/mcp", + enableDnsRebindingProtection: Boolean, + allowedHosts: List?, + allowedOrigins: List?, + configuration: StreamableHttpServerTransport.Configuration, + block: RoutingContext.() -> Server, +) { + installMcpContentNegotiation() + + routing { + route(path) { + installDnsRebindingProtection(enableDnsRebindingProtection, allowedHosts, allowedOrigins) + + post { + mcpStatelessStreamableHttpEndpoint( + configuration = configuration, + block = block, + ) + } + get { + call.rejectUnsupportedMethod() + } + delete { + call.rejectUnsupportedMethod() + } + } + } +} + +/** + * Configures the Ktor Application to handle Model Context Protocol (MCP) + * over _stateless_ [Streamable HTTP Transport](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http) + * + * Sets up an HTTP POST endpoint at [path]. GET and DELETE requests return 405 Method Not Allowed. + * Every request/response pair is returned as JSON, so this endpoint opens no SSE stream and + * offers no resumability. Use [mcpStreamableHttp] when either is required. + * + * Automatically installs [ContentNegotiation][io.ktor.server.plugins.contentnegotiation.ContentNegotiation] + * with [McpJson][io.modelcontextprotocol.kotlin.sdk.types.McpJson]. + * + * @param path The URL path where the server listens for incoming JSON-RPC requests. Defaults to "/mcp". + * @param enableDnsRebindingProtection Determines whether DNS rebinding protection is enabled. Defaults to `true`. + * @param allowedHosts A list of allowed hostnames. If `null` and DNS rebinding protection is enabled, + * defaults to `localhost`, `127.0.0.1`, `[::1]`. + * @param allowedOrigins A list of allowed `Origin` header values, compared by hostname only + * (scheme and port are ignored). Requests without an `Origin` header are allowed. + * If `null`, origin validation is disabled. + * @param block factory block with access to the [RoutingContext] (for reading request headers) + * that creates and returns the [Server] to handle the connection. + */ +@KtorDsl +public fun Application.mcpStatelessStreamableHttp( + path: String = "/mcp", + enableDnsRebindingProtection: Boolean = true, + allowedHosts: List? = null, + allowedOrigins: List? = null, + block: RoutingContext.() -> Server, +) { + mcpStatelessStreamableHttp( + path = path, + enableDnsRebindingProtection = enableDnsRebindingProtection, + allowedHosts = allowedHosts, + allowedOrigins = allowedOrigins, + configuration = StreamableHttpServerTransport.Configuration( + enableJsonResponse = true, + ), + block = block, + ) +} + +/** + * Retained only so that existing call sites get a migration hint instead of an unresolved parameter name. + * + * @param eventStore never consulted by a stateless endpoint. Resumption is driven by a `GET` carrying + * `Last-Event-ID`, and this endpoint answers `GET` with `405 Method Not Allowed`, so no stream + * exists to store events on or replay them to. Use [mcpStreamableHttp] when you need resumability. + */ +@Deprecated( + "Use mcpStatelessStreamableHttp without eventStore.", + ReplaceWith("mcpStatelessStreamableHttp(path, enableDnsRebindingProtection, allowedHosts, allowedOrigins, block)"), + DeprecationLevel.ERROR, +) +@Suppress("UnusedParameter") +public fun Application.mcpStatelessStreamableHttp( + path: String = "/mcp", + enableDnsRebindingProtection: Boolean = true, + allowedHosts: List? = null, + allowedOrigins: List? = null, + eventStore: EventStore?, + block: RoutingContext.() -> Server, +) { + mcpStatelessStreamableHttp( + path = path, + enableDnsRebindingProtection = enableDnsRebindingProtection, + allowedHosts = allowedHosts, + allowedOrigins = allowedOrigins, + block = block, + ) +} + private suspend fun ServerSSESession.mcpSseEndpoint( postEndpoint: String, - transports: ConcurrentMap, + transportManager: TransportManager, + maxRequestBodySize: Long, block: ServerSSESession.() -> Server, ) { - val transport = mcpSseTransport(postEndpoint, transports) + val transport = mcpSseTransport(postEndpoint, transportManager, maxRequestBodySize) val server = block() server.onClose { logger.info { "Server connection closed for sessionId: ${transport.sessionId}" } - transports.remove(transport.sessionId) + transportManager.removeTransport(transport.sessionId) } - server.connect(transport) + server.createSession(transport) + logger.debug { "Server connected to transport for sessionId: ${transport.sessionId}" } + + awaitCancellation() } -internal fun ServerSSESession.mcpSseTransport( +private fun ServerSSESession.mcpSseTransport( postEndpoint: String, - transports: ConcurrentMap, + transportManager: TransportManager, + maxRequestBodySize: Long, ): SseServerTransport { - val transport = SseServerTransport(postEndpoint, this) - transports[transport.sessionId] = transport - + val transport = SseServerTransport(postEndpoint, this, maxRequestBodySize) + transportManager.addTransport(transport.sessionId, transport) logger.info { "New SSE connection established and stored with sessionId: ${transport.sessionId}" } return transport } -internal suspend fun RoutingContext.mcpPostEndpoint(transports: ConcurrentMap) { - val sessionId: String = call.request.queryParameters["sessionId"] - ?: run { - call.respond(HttpStatusCode.BadRequest, "sessionId query parameter is not provided") - return - } +private suspend fun RoutingContext.mcpStatelessStreamableHttpEndpoint( + configuration: StreamableHttpServerTransport.Configuration, + block: RoutingContext.() -> Server, +) { + val transport = StreamableHttpServerTransport( + configuration, + ).also { it.setSessionIdGenerator(null) } + + logger.info { "New stateless StreamableHttp connection established without sessionId" } + + val server = block() + val session = server.createSession(transport) + + try { + transport.handleRequest(null, this.call) + logger.debug { "Server connected to transport without sessionId" } + } finally { + // A stateless session serves exactly one request: close it so the server's + // session registry and notification subscriptions do not grow unboundedly. + session.close() + logger.debug { "Stateless session closed after request completion" } + } +} + +private suspend fun RoutingContext.mcpPostEndpoint(transportManager: TransportManager) { + val sessionId: String = call.request.queryParameters["sessionId"] ?: run { + call.respond(HttpStatusCode.BadRequest, "sessionId query parameter is not provided") + return + } logger.debug { "Received message for sessionId: $sessionId" } - val transport = transports[sessionId] + val transport = transportManager.getTransport(sessionId) if (transport == null) { logger.warn { "Session not found for sessionId: $sessionId" } call.respond(HttpStatusCode.NotFound, "Session not found") @@ -113,3 +438,86 @@ internal suspend fun RoutingContext.mcpPostEndpoint(transports: ConcurrentMap, +): StreamableHttpServerTransport? { + val sessionId = call.request.sessionId() + if (sessionId.isNullOrEmpty()) { + call.reject( + HttpStatusCode.BadRequest, + RPCError.ErrorCode.CONNECTION_CLOSED, + "Bad Request: No valid session ID provided", + ) + return null + } + + val transport = transportManager.getTransport(sessionId) + return if (transport == null) { + call.reject( + HttpStatusCode.NotFound, + RPCError.ErrorCode.CONNECTION_CLOSED, + "Session not found", + ) + null + } else { + transport + } +} + +private suspend fun RoutingContext.streamableTransport( + transportManager: TransportManager, + configuration: StreamableHttpServerTransport.Configuration, + block: RoutingContext.() -> Server, +): StreamableHttpServerTransport? { + val sessionId = call.request.sessionId() + if (sessionId != null) { + val transport = transportManager.getTransport(sessionId) + return transport ?: existingStreamableTransport(call, transportManager) + } + + val transport = StreamableHttpServerTransport(configuration) + + transport.setOnSessionInitialized { initializedSessionId -> + transportManager.addTransport(initializedSessionId, transport) + logger.info { "New StreamableHttp connection established and stored with sessionId: $initializedSessionId" } + } + + transport.setOnSessionClosed { closedSession -> + transportManager.removeTransport(closedSession) + logger.info { "Closed StreamableHttp connection and removed sessionId: $closedSession" } + } + + val server = block() + server.onClose { + transport.sessionId?.let { transportManager.removeTransport(it) } + logger.info { "Server connection closed for sessionId: ${transport.sessionId}" } + } + server.createSession(transport) + + return transport +} + +private fun Route.installDnsRebindingProtection(enabled: Boolean, hosts: List?, origins: List?) { + if (!enabled) return + install(DnsRebindingProtection) { + allowedHosts = hosts ?: LOCALHOST_ALLOWED_HOSTS + // Secure-by-default: when relying on the localhost host defaults, validate the Origin + // header against localhost too, so a request with a valid Host but a hostile Origin + // (e.g. a DNS-rebinding page) is rejected. Callers with custom hosts opt in explicitly. + allowedOrigins = origins ?: LOCALHOST_ALLOWED_ORIGINS.takeIf { hosts == null } + } +} diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServerHelpers.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServerHelpers.kt new file mode 100644 index 0000000..42093c8 --- /dev/null +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServerHelpers.kt @@ -0,0 +1,46 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.github.oshai.kotlinlogging.KotlinLogging +import io.ktor.serialization.kotlinx.json.json +import io.ktor.server.application.Application +import io.ktor.server.application.install +import io.ktor.server.application.pluginOrNull +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation +import io.ktor.util.AttributeKey +import io.modelcontextprotocol.kotlin.sdk.types.McpJson + +private val logger = KotlinLogging.logger {} + +private val McpContentNegotiationInstalled = AttributeKey("McpContentNegotiationInstalled") + +/** + * Installs [ContentNegotiation] configured with [McpJson]. + * + * MCP requires specific JSON settings (`explicitNulls = false`, `encodeDefaults = true`, + * `classDiscriminatorMode = NONE`). Using the default `json()` instead of `json(McpJson)` + * causes serialization failures at runtime (e.g., explicit null fields in JSON-RPC responses). + * + * If [ContentNegotiation] was already installed by user code, a warning is logged because + * Ktor's public API does not expose the Json configuration for verification. + * + * This function is idempotent: subsequent calls on the same [Application] return immediately + * without re-installing the plugin or logging additional warnings. + */ +internal fun Application.installMcpContentNegotiation() { + if (attributes.getOrNull(McpContentNegotiationInstalled) != null) return + attributes.put(McpContentNegotiationInstalled, Unit) + + if (pluginOrNull(ContentNegotiation) != null) { + // ContentNegotiation was already installed by user code. + logger.warn { + "ContentNegotiation is already installed. " + + "MCP requires json(McpJson) for correct JSON-RPC serialization " + + "(explicitNulls = false, encodeDefaults = true). " + + "Ensure your ContentNegotiation configuration includes json(McpJson)." + } + } else { + install(ContentNegotiation) { + json(McpJson) + } + } +} diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/RequestBody.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/RequestBody.kt new file mode 100644 index 0000000..0cc4143 --- /dev/null +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/RequestBody.kt @@ -0,0 +1,34 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.ktor.server.application.ApplicationCall +import io.ktor.server.request.receiveChannel +import io.ktor.utils.io.readRemaining +import kotlinx.io.Buffer +import kotlinx.io.IOException +import kotlinx.io.readString + +/** Default maximum size (in bytes) for an incoming HTTP request body: 4 MiB. */ +internal const val DEFAULT_MAX_REQUEST_BODY_SIZE: Long = 4L * 1024 * 1024 + +/** Signals that an incoming request body exceeded the maximum size a transport is willing to read. */ +internal class RequestBodyTooLargeException(val maxBodySize: Long) : + IOException("Request body exceeds the maximum of $maxBodySize bytes.") + +/** + * Reads the request body as a UTF-8 string, rejecting bodies larger than [maxBytes]. + * + * At most `maxBytes + 1` bytes are ever pulled from the request channel, so an oversized or unframed + * body cannot exhaust memory regardless of the (untrusted) `Content-Length` header. + * + * @throws RequestBodyTooLargeException if the body exceeds [maxBytes] + */ +internal suspend fun ApplicationCall.receiveTextWithLimit(maxBytes: Long): String { + val buffer = Buffer() + val readLimit = if (maxBytes == Long.MAX_VALUE) maxBytes else maxBytes + 1 + val read = receiveChannel().readRemaining(readLimit).transferTo(buffer) + if (read > maxBytes) { + buffer.clear() + throw RequestBodyTooLargeException(maxBytes) + } + return buffer.readString() +} diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt index 8b5c6be..f525ec2 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt @@ -5,33 +5,45 @@ import io.ktor.http.HttpStatusCode import io.ktor.http.encodeURLPath import io.ktor.server.application.ApplicationCall import io.ktor.server.request.contentType -import io.ktor.server.request.receiveText import io.ktor.server.response.respondText import io.ktor.server.sse.ServerSSESession -import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport -import io.modelcontextprotocol.kotlin.sdk.shared.McpJson +import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.McpJson +import kotlinx.coroutines.InternalCoroutinesApi import kotlinx.coroutines.job import kotlin.concurrent.atomics.AtomicBoolean import kotlin.concurrent.atomics.ExperimentalAtomicApi +import kotlin.coroutines.cancellation.CancellationException import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid internal const val SESSION_ID_PARAM = "sessionId" -@Deprecated("Use SseServerTransport instead", ReplaceWith("SseServerTransport"), DeprecationLevel.WARNING) -public typealias SSEServerTransport = SseServerTransport - /** * Server transport for SSE: this will send messages over an SSE connection and receive messages from HTTP POST requests. * * Creates a new SSE server transport, which will direct the client to POST messages to the relative or absolute URL identified by `_endpoint`. + * + * @param endpoint relative or absolute URL the client will POST messages to + * @param session active SSE session used to deliver server-to-client events + * @param maxRequestBodySize maximum allowed size, in bytes, of an incoming POST body; larger requests are + * rejected with `413 Payload Too Large` without being buffered in full. Defaults to 4 MiB. */ @OptIn(ExperimentalAtomicApi::class) -public class SseServerTransport(private val endpoint: String, private val session: ServerSSESession) : - AbstractTransport() { +public class SseServerTransport( + private val endpoint: String, + private val session: ServerSSESession, + private val maxRequestBodySize: Long = DEFAULT_MAX_REQUEST_BODY_SIZE, +) : AbstractTransport() { + init { + require(maxRequestBodySize > 0) { "maxRequestBodySize must be greater than 0" } + } + private val initialized: AtomicBoolean = AtomicBoolean(false) + /** Unique identifier for this transport session, generated randomly on creation. */ @OptIn(ExperimentalUuidApi::class) public val sessionId: String = Uuid.random().toString() @@ -53,10 +65,13 @@ public class SseServerTransport(private val endpoint: String, private val sessio data = "${endpoint.encodeURLPath()}?$SESSION_ID_PARAM=$sessionId", ) - try { - session.coroutineContext.job.join() - } finally { - _onClose.invoke() + @OptIn(InternalCoroutinesApi::class) + session.coroutineContext.job.invokeOnCompletion { + if (it != null && it !is CancellationException) { + _onError.invoke(it) + } else { + invokeOnCloseCallback() + } } } @@ -70,15 +85,22 @@ public class SseServerTransport(private val endpoint: String, private val sessio val message = "SSE connection not established" call.respondText(message, status = HttpStatusCode.InternalServerError) _onError.invoke(IllegalStateException(message)) + return } val body = try { val ct = call.request.contentType() - if (ct != ContentType.Application.Json) { + if (!ct.match(ContentType.Application.Json)) { error("Unsupported content-type: $ct") } - call.receiveText() + call.receiveTextWithLimit(maxRequestBodySize) + } catch (e: CancellationException) { + throw e + } catch (e: RequestBodyTooLargeException) { + call.respondText(e.message ?: "Request body too large", status = HttpStatusCode.PayloadTooLarge) + _onError.invoke(e) + return } catch (e: Exception) { call.respondText("Invalid message: ${e.message}", status = HttpStatusCode.BadRequest) _onError.invoke(e) @@ -87,6 +109,8 @@ public class SseServerTransport(private val endpoint: String, private val sessio try { handleMessage(body) + } catch (e: CancellationException) { + throw e } catch (e: Exception) { call.respondText("Error handling message $body: ${e.message}", status = HttpStatusCode.BadRequest) return @@ -103,6 +127,8 @@ public class SseServerTransport(private val endpoint: String, private val sessio try { val parsedMessage = McpJson.decodeFromString(message) _onMessage.invoke(parsedMessage) + } catch (e: CancellationException) { + throw e } catch (e: Exception) { _onError.invoke(e) throw e @@ -111,10 +137,10 @@ public class SseServerTransport(private val endpoint: String, private val sessio override suspend fun close() { session.close() - _onClose.invoke() + invokeOnCloseCallback() } - override suspend fun send(message: JSONRPCMessage) { + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { if (!initialized.load()) { error("Not connected") } diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingValidation.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingValidation.kt new file mode 100644 index 0000000..8243171 --- /dev/null +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingValidation.kt @@ -0,0 +1,54 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.modelcontextprotocol.kotlin.sdk.types.SamplingMessage +import io.modelcontextprotocol.kotlin.sdk.types.ToolResultContent +import io.modelcontextprotocol.kotlin.sdk.types.ToolUseContent + +/** + * Validates tool_use / tool_result rules on the last two messages of [messages]. + * + * Only the boundary between the previous message and the final message matters, because + * earlier turns were already validated when they were appended; the sole freshly-built + * portion of a sampling request is its tail. + * + * Rules enforced: + * + * 1. If the last message contains any `tool_result` block, it MUST contain only + * `tool_result` blocks (no mixing with text/image/audio/tool_use). + * 2. If the last message contains any `tool_result`, the previous message MUST contain + * matching `tool_use` blocks. + * 3. If the previous message contains `tool_use` blocks, the last message's + * `tool_result` ids MUST form exactly the same set. + * + * On the first violation throws [IllegalArgumentException]. No-op when no + * tool_use / tool_result blocks are involved. + */ +internal fun validateSamplingMessages(messages: List) { + if (messages.isEmpty()) return + + val last = messages.last().content + val hasToolResult = last.any { it is ToolResultContent } + + val previous = messages.getOrNull(messages.size - 2)?.content.orEmpty() + val hasPreviousToolUse = previous.any { it is ToolUseContent } + + if (hasToolResult) { + require(last.all { it is ToolResultContent }) { + "The last message must contain only tool_result content if any is present" + } + require(hasPreviousToolUse) { + "tool_result blocks are not matching any tool_use from the previous message" + } + } + + if (hasPreviousToolUse) { + val toolUseIds = previous.filterIsInstance().map { it.id }.toSet() + val toolResultIds = last.filterIsInstance().map { it.toolUseId }.toSet() + require(toolResultIds.isNotEmpty()) { + "tool_use blocks from previous message must be followed by matching tool_result blocks" + } + require(toolUseIds == toolResultIds) { + "ids of tool_result blocks and tool_use blocks from previous message do not match" + } + } +} diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt index ac71b5f..6f7711f 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt @@ -1,60 +1,65 @@ package io.modelcontextprotocol.kotlin.sdk.server import io.github.oshai.kotlinlogging.KotlinLogging -import io.modelcontextprotocol.kotlin.sdk.CallToolRequest -import io.modelcontextprotocol.kotlin.sdk.CallToolResult -import io.modelcontextprotocol.kotlin.sdk.ClientCapabilities -import io.modelcontextprotocol.kotlin.sdk.CreateElicitationRequest -import io.modelcontextprotocol.kotlin.sdk.CreateElicitationRequest.RequestedSchema -import io.modelcontextprotocol.kotlin.sdk.CreateElicitationResult -import io.modelcontextprotocol.kotlin.sdk.CreateMessageRequest -import io.modelcontextprotocol.kotlin.sdk.CreateMessageResult -import io.modelcontextprotocol.kotlin.sdk.EmptyJsonObject -import io.modelcontextprotocol.kotlin.sdk.EmptyRequestResult -import io.modelcontextprotocol.kotlin.sdk.GetPromptRequest -import io.modelcontextprotocol.kotlin.sdk.GetPromptResult -import io.modelcontextprotocol.kotlin.sdk.Implementation -import io.modelcontextprotocol.kotlin.sdk.InitializeRequest -import io.modelcontextprotocol.kotlin.sdk.InitializeResult -import io.modelcontextprotocol.kotlin.sdk.InitializedNotification -import io.modelcontextprotocol.kotlin.sdk.LATEST_PROTOCOL_VERSION -import io.modelcontextprotocol.kotlin.sdk.ListPromptsRequest -import io.modelcontextprotocol.kotlin.sdk.ListPromptsResult -import io.modelcontextprotocol.kotlin.sdk.ListResourceTemplatesRequest -import io.modelcontextprotocol.kotlin.sdk.ListResourceTemplatesResult -import io.modelcontextprotocol.kotlin.sdk.ListResourcesRequest -import io.modelcontextprotocol.kotlin.sdk.ListResourcesResult -import io.modelcontextprotocol.kotlin.sdk.ListRootsRequest -import io.modelcontextprotocol.kotlin.sdk.ListRootsResult -import io.modelcontextprotocol.kotlin.sdk.ListToolsRequest -import io.modelcontextprotocol.kotlin.sdk.ListToolsResult -import io.modelcontextprotocol.kotlin.sdk.LoggingMessageNotification -import io.modelcontextprotocol.kotlin.sdk.Method -import io.modelcontextprotocol.kotlin.sdk.PingRequest -import io.modelcontextprotocol.kotlin.sdk.Prompt -import io.modelcontextprotocol.kotlin.sdk.PromptArgument -import io.modelcontextprotocol.kotlin.sdk.PromptListChangedNotification -import io.modelcontextprotocol.kotlin.sdk.ReadResourceRequest -import io.modelcontextprotocol.kotlin.sdk.ReadResourceResult -import io.modelcontextprotocol.kotlin.sdk.Resource -import io.modelcontextprotocol.kotlin.sdk.ResourceListChangedNotification -import io.modelcontextprotocol.kotlin.sdk.ResourceUpdatedNotification -import io.modelcontextprotocol.kotlin.sdk.SUPPORTED_PROTOCOL_VERSIONS -import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities -import io.modelcontextprotocol.kotlin.sdk.Tool -import io.modelcontextprotocol.kotlin.sdk.ToolAnnotations -import io.modelcontextprotocol.kotlin.sdk.ToolListChangedNotification -import io.modelcontextprotocol.kotlin.sdk.shared.Protocol +import io.modelcontextprotocol.kotlin.sdk.server.utils.warnIfInvalidToolName import io.modelcontextprotocol.kotlin.sdk.shared.ProtocolOptions import io.modelcontextprotocol.kotlin.sdk.shared.RequestOptions -import kotlinx.atomicfu.atomic -import kotlinx.atomicfu.getAndUpdate -import kotlinx.atomicfu.update -import kotlinx.collections.immutable.minus -import kotlinx.collections.immutable.persistentMapOf -import kotlinx.collections.immutable.toPersistentSet -import kotlinx.coroutines.CompletableDeferred +import io.modelcontextprotocol.kotlin.sdk.shared.Transport +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest +import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageResult +import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.ElicitResult +import io.modelcontextprotocol.kotlin.sdk.types.ElicitationCompleteNotification +import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject +import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.ListPromptsRequest +import io.modelcontextprotocol.kotlin.sdk.types.ListPromptsResult +import io.modelcontextprotocol.kotlin.sdk.types.ListResourceTemplatesRequest +import io.modelcontextprotocol.kotlin.sdk.types.ListResourceTemplatesResult +import io.modelcontextprotocol.kotlin.sdk.types.ListResourcesRequest +import io.modelcontextprotocol.kotlin.sdk.types.ListResourcesResult +import io.modelcontextprotocol.kotlin.sdk.types.ListRootsResult +import io.modelcontextprotocol.kotlin.sdk.types.ListToolsRequest +import io.modelcontextprotocol.kotlin.sdk.types.ListToolsResult +import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.Notification +import io.modelcontextprotocol.kotlin.sdk.types.Prompt +import io.modelcontextprotocol.kotlin.sdk.types.PromptArgument +import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult +import io.modelcontextprotocol.kotlin.sdk.types.Resource +import io.modelcontextprotocol.kotlin.sdk.types.ResourceTemplate +import io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotification +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.SubscribeRequest +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.Tool +import io.modelcontextprotocol.kotlin.sdk.types.ToolAnnotations +import io.modelcontextprotocol.kotlin.sdk.types.ToolExecution +import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema +import io.modelcontextprotocol.kotlin.sdk.types.UnsubscribeRequest +import io.modelcontextprotocol.kotlin.sdk.types.UrlElicitationRequiredException +import io.modelcontextprotocol.kotlin.sdk.utils.MatchResult +import io.modelcontextprotocol.kotlin.sdk.utils.PathSegmentTemplateMatcher +import io.modelcontextprotocol.kotlin.sdk.utils.ResourceTemplateMatcher +import io.modelcontextprotocol.kotlin.sdk.utils.ResourceTemplateMatcherFactory +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlin.coroutines.CoroutineContext +import kotlin.jvm.JvmOverloads +import kotlin.time.ExperimentalTime private val logger = KotlinLogging.logger {} @@ -62,104 +67,223 @@ private val logger = KotlinLogging.logger {} * Configuration options for the MCP server. * * @property capabilities The capabilities this server supports. - * @property enforceStrictCapabilities Whether to strictly enforce capabilities when interacting with clients. + * @param enforceStrictCapabilities Whether to strictly enforce capabilities when interacting with clients. + * Forwarded to [ProtocolOptions.enforceStrictCapabilities]. + * @property resourceTemplateMatcherFactory The factory used to create [ResourceTemplateMatcher] instances + * for matching resource URIs against registered templates. Defaults to [PathSegmentTemplateMatcher.factory]. + * @param handlerCoroutineContext Coroutine context for inbound handlers. See [ProtocolOptions.handlerCoroutineContext]. */ -public class ServerOptions(public val capabilities: ServerCapabilities, enforceStrictCapabilities: Boolean = true) : - ProtocolOptions(enforceStrictCapabilities = enforceStrictCapabilities) +public class ServerOptions( + public val capabilities: ServerCapabilities, + enforceStrictCapabilities: Boolean = true, + public val resourceTemplateMatcherFactory: ResourceTemplateMatcherFactory = PathSegmentTemplateMatcher.factory, + handlerCoroutineContext: CoroutineContext = Dispatchers.Default, +) : ProtocolOptions( + enforceStrictCapabilities = enforceStrictCapabilities, + handlerCoroutineContext = handlerCoroutineContext, +) { + @JvmOverloads + public constructor( + capabilities: ServerCapabilities, + enforceStrictCapabilities: Boolean = true, + ) : this(capabilities, enforceStrictCapabilities, PathSegmentTemplateMatcher.factory) +} /** - * An MCP server on top of a pluggable transport. + * An MCP server is responsible for storing features and handling new connections. * * This server automatically responds to the initialization flow as initiated by the client. * You can register tools, prompts, and resources using [addTool], [addPrompt], and [addResource]. * The server will then automatically handle listing and retrieval requests from the client. * - * @param serverInfo Information about this server implementation (name, version). - * @param options Configuration options for the server. + * In case the server supports feature list notification or resource substitution, + * the server will automatically send notifications for all connected clients. + * Currently, after subscription to a resource, the server will NOT send the subscription confirmation + * as this response schema is not defined in the protocol. + * + * @property serverInfo Information about this server implementation (name, version). + * @property options Configuration options for the server. + * @property instructionsProvider Optional provider for instructions from the server to the client about how to use + * this server. The provider is called each time a new session is started to support dynamic instructions. + * @param block A block to configure the mcp server. */ -public open class Server(private val serverInfo: Implementation, options: ServerOptions) : Protocol(options) { - @Suppress("ktlint:standard:backing-property-naming") - private var _onInitialized: (() -> Unit) = {} +public open class Server( + protected val serverInfo: Implementation, + protected val options: ServerOptions, + protected val instructionsProvider: (() -> String)? = null, + block: Server.() -> Unit = {}, +) { + /** + * Alternative constructor that provides the instructions directly as a string. + * + * @param serverInfo Information about this server implementation (name, version). + * @param options Configuration options for the server. + * @param instructions Instructions from the server to the client about how to use this server. + * @param block A block to configure the mcp server. + */ + public constructor( + serverInfo: Implementation, + options: ServerOptions, + instructions: String, + block: Server.() -> Unit = {}, + ) : this(serverInfo, options, { instructions }, block) + + private var _onConnect: (() -> Unit) = {} - @Suppress("ktlint:standard:backing-property-naming") private var _onClose: () -> Unit = {} - /** - * The client's reported capabilities after initialization. - */ - public var clientCapabilities: ClientCapabilities? = null - private set + @OptIn(ExperimentalTime::class) + private val notificationService = FeatureNotificationService() - /** - * The client's version information after initialization. - */ - public var clientVersion: Implementation? = null - private set + private val sessionRegistry = ServerSessionRegistry() - private val capabilities: ServerCapabilities = options.capabilities - - private val _tools = atomic(persistentMapOf()) - private val _prompts = atomic(persistentMapOf()) - private val _resources = atomic(persistentMapOf()) - public val tools: Map - get() = _tools.value - public val prompts: Map - get() = _prompts.value - public val resources: Map - get() = _resources.value - - init { - logger.debug { "Initializing MCP server with capabilities: $capabilities" } - - // Core protocol handlers - setRequestHandler(Method.Defined.Initialize) { request, _ -> - handleInitialize(request) + private val toolRegistry = FeatureRegistry("Tool").apply { + if (options.capabilities.tools?.listChanged ?: false) { + addListener(notificationService.toolListChangedListener) } - setNotificationHandler(Method.Defined.NotificationsInitialized) { - _onInitialized() - CompletableDeferred(Unit) + } + private val promptRegistry = FeatureRegistry("Prompt").apply { + if (options.capabilities.prompts?.listChanged ?: false) { + addListener(notificationService.promptListChangedListener) } - - // Internal handlers for tools - if (capabilities.tools != null) { - setRequestHandler(Method.Defined.ToolsList) { _, _ -> - handleListTools() - } - setRequestHandler(Method.Defined.ToolsCall) { request, _ -> - handleCallTool(request) - } + } + private val resourceRegistry = FeatureRegistry("Resource").apply { + if (options.capabilities.resources?.listChanged ?: false) { + addListener(notificationService.resourceListChangedListener) } - - // Internal handlers for prompts - if (capabilities.prompts != null) { - setRequestHandler(Method.Defined.PromptsList) { _, _ -> - handleListPrompts() - } - setRequestHandler(Method.Defined.PromptsGet) { request, _ -> - handleGetPrompt(request) - } + if (options.capabilities.resources?.subscribe ?: false) { + addListener(notificationService.resourceUpdatedListener) } - - // Internal handlers for resources - if (capabilities.resources != null) { - setRequestHandler(Method.Defined.ResourcesList) { _, _ -> - handleListResources() - } - setRequestHandler(Method.Defined.ResourcesRead) { request, _ -> - handleReadResource(request) - } - setRequestHandler(Method.Defined.ResourcesTemplatesList) { _, _ -> - handleListResourceTemplates() - } + } + private val resourceTemplateRegistry = FeatureRegistry("ResourceTemplate").apply { + if (options.capabilities.resources?.listChanged == true) { + addListener(notificationService.resourceListChangedListener) } } /** - * Registers a callback to be invoked when the server has completed initialization. + * Provides a snapshot of all sessions currently registered in the server */ - public fun onInitialized(block: () -> Unit) { - val old = _onInitialized - _onInitialized = { + public val sessions: Map + get() = sessionRegistry.sessions + + /** + * Provides a snapshot of all tools currently registered in the server + */ + public val tools: Map + get() = toolRegistry.values + + /** + * Provides a snapshot of all prompts currently registered in the server + */ + public val prompts: Map + get() = promptRegistry.values + + /** + * Provides a snapshot of all resources currently registered in the server + */ + public val resources: Map + get() = resourceRegistry.values + + /** + * Provides a snapshot of all resource templates currently registered in the server. + */ + public val resourceTemplates: List + get() = resourceTemplateRegistry.values.values.map { it.resourceTemplate } + + init { + block(this) + } + + /** Closes this server, shutting down the notification service and all active sessions. */ + public suspend fun close() { + logger.debug { "Closing MCP server" } + notificationService.close() + sessions.forEach { (sessionId, session) -> + logger.info { "Closing session $sessionId" } + session.close() + } + _onClose() + } + + /** + * Starts a new server session with the given transport and initializes + * internal request handlers based on the server's capabilities. + * + * @param transport The transport layer to connect the session with. + * @return The initialized and connected server session. + */ + public suspend fun createSession(transport: Transport): ServerSession { + val session = ServerSession(serverInfo, options, instructionsProvider?.invoke()) + + // Internal handlers for tools + if (options.capabilities.tools != null) { + session.setRequestHandler(Method.Defined.ToolsList) { _, _ -> + handleListTools() + } + session.setRequestHandler(Method.Defined.ToolsCall) { request, _ -> + handleCallTool(session, request) + } + } + + // Internal handlers for prompts + if (options.capabilities.prompts != null) { + session.setRequestHandler(Method.Defined.PromptsList) { _, _ -> + handleListPrompts() + } + session.setRequestHandler(Method.Defined.PromptsGet) { request, _ -> + handleGetPrompt(session, request) + } + } + + // Internal handlers for resources + if (options.capabilities.resources != null) { + session.setRequestHandler(Method.Defined.ResourcesList) { _, _ -> + handleListResources() + } + session.setRequestHandler(Method.Defined.ResourcesRead) { request, _ -> + handleReadResource(session, request) + } + session.setRequestHandler(Method.Defined.ResourcesTemplatesList) { _, _ -> + handleListResourceTemplates() + } + if (options.capabilities.resources?.subscribe ?: false) { + session.setRequestHandler(Method.Defined.ResourcesSubscribe) { request, _ -> + handleSubscribeResources(session, request) + // Does not return any confirmation as the structure is not stated in the protocol + null + } + session.setRequestHandler(Method.Defined.ResourcesUnsubscribe) { request, _ -> + handleUnsubscribeResources(session, request) + // Does not return any confirmation as the structure is not stated in the protocol + null + } + } + } + + // Register cleanup handler to remove session from list when it closes + session.onClose { + logger.debug { "Removing closed session from active sessions list" } + notificationService.unsubscribeSession(session) + sessionRegistry.removeSession(session.sessionId) + } + + logger.debug { "Server session connecting to transport" } + session.connect(transport) + logger.debug { "Server session successfully connected to transport" } + sessionRegistry.addSession(session) + notificationService.subscribeSession(session) + + _onConnect() + return session + } + + /** + * Registers a callback to be invoked when the new server session connected. + */ + public fun onConnect(block: () -> Unit) { + val old = _onConnect + _onConnect = { old() block() } @@ -176,14 +300,6 @@ public open class Server(private val serverInfo: Implementation, options: Server } } - /** - * Called when the server connection is closing. - */ - override fun onClose() { - logger.info { "Server connection closing" } - _onClose() - } - /** * Registers a single tool. The client can then call this tool. * @@ -191,37 +307,50 @@ public open class Server(private val serverInfo: Implementation, options: Server * @param handler A suspend function that handles executing the tool when called by the client. * @throws IllegalStateException If the server does not support tools. */ - public fun addTool(tool: Tool, handler: suspend (CallToolRequest) -> CallToolResult) { - if (capabilities.tools == null) { + public fun addTool(tool: Tool, handler: suspend ClientConnection.(CallToolRequest) -> CallToolResult) { + checkNotNull(options.capabilities.tools) { logger.error { "Failed to add tool '${tool.name}': Server does not support tools capability" } - throw IllegalStateException("Server does not support tools capability. Enable it in ServerOptions.") + "Server does not support tools capability. Enable it in ServerOptions." } - logger.info { "Registering tool: ${tool.name}" } - _tools.update { current -> current.put(tool.name, RegisteredTool(tool, handler)) } + warnIfInvalidToolName(tool.name) + toolRegistry.add(RegisteredTool(tool, handler)) } /** * Registers a single tool. The client can then call this tool. * * @param name The name of the tool. - * @param title An optional human-readable name of the tool for display purposes. * @param description A human-readable description of what the tool does. * @param inputSchema The expected input schema for the tool. + * @param title An optional human-readable name of the tool for display purposes. * @param outputSchema The optional expected output schema for the tool. * @param toolAnnotations Optional additional tool information. + * @param execution Optional execution-related properties, such as task-augmented execution support. + * @param meta Optional metadata as a [JsonObject]. * @param handler A suspend function that handles executing the tool when called by the client. * @throws IllegalStateException If the server does not support tools. */ public fun addTool( name: String, description: String, - inputSchema: Tool.Input = Tool.Input(), + inputSchema: ToolSchema = ToolSchema(), title: String? = null, - outputSchema: Tool.Output? = null, + outputSchema: ToolSchema? = null, toolAnnotations: ToolAnnotations? = null, - handler: suspend (CallToolRequest) -> CallToolResult, + execution: ToolExecution? = null, + meta: JsonObject? = null, + handler: suspend ClientConnection.(CallToolRequest) -> CallToolResult, ) { - val tool = Tool(name, title, description, inputSchema, outputSchema, toolAnnotations) + val tool = Tool( + name = name, + inputSchema = inputSchema, + outputSchema = outputSchema, + description = description, + title = title, + annotations = toolAnnotations, + execution = execution, + meta = meta, + ) addTool(tool, handler) } @@ -232,12 +361,12 @@ public open class Server(private val serverInfo: Implementation, options: Server * @throws IllegalStateException If the server does not support tools. */ public fun addTools(toolsToAdd: List) { - if (capabilities.tools == null) { + checkNotNull(options.capabilities.tools) { logger.error { "Failed to add tools: Server does not support tools capability" } - throw IllegalStateException("Server does not support tools capability.") + "Server does not support tools capability." } - logger.info { "Registering ${toolsToAdd.size} tools" } - _tools.update { current -> current.putAll(toolsToAdd.associateBy { it.tool.name }) } + toolsToAdd.forEach { warnIfInvalidToolName(it.tool.name) } + toolRegistry.addAll(toolsToAdd) } /** @@ -248,23 +377,11 @@ public open class Server(private val serverInfo: Implementation, options: Server * @throws IllegalStateException If the server does not support tools. */ public fun removeTool(name: String): Boolean { - if (capabilities.tools == null) { + checkNotNull(options.capabilities.tools) { logger.error { "Failed to remove tool '$name': Server does not support tools capability" } - throw IllegalStateException("Server does not support tools capability.") + "Server does not support tools capability." } - logger.info { "Removing tool: $name" } - - val oldMap = _tools.getAndUpdate { current -> current.remove(name) } - - val removed = name in oldMap - logger.debug { - if (removed) { - "Tool removed: $name" - } else { - "Tool not found: $name" - } - } - return removed + return toolRegistry.remove(name) } /** @@ -275,22 +392,12 @@ public open class Server(private val serverInfo: Implementation, options: Server * @throws IllegalStateException If the server does not support tools. */ public fun removeTools(toolNames: List): Int { - if (capabilities.tools == null) { + checkNotNull(options.capabilities.tools) { logger.error { "Failed to remove tools: Server does not support tools capability" } - throw IllegalStateException("Server does not support tools capability.") + "Server does not support tools capability." } - logger.info { "Removing ${toolNames.size} tools" } - val oldMap = _tools.getAndUpdate { current -> current - toolNames.toPersistentSet() } - - val removedCount = toolNames.count { it in oldMap } - logger.info { - if (removedCount > 0) { - "Removed $removedCount tools" - } else { - "No tools were removed" - } - } + val removedCount = toolRegistry.removeAll(toolNames) return removedCount } @@ -301,13 +408,15 @@ public open class Server(private val serverInfo: Implementation, options: Server * @param promptProvider A suspend function that returns the prompt content when requested by the client. * @throws IllegalStateException If the server does not support prompts. */ - public fun addPrompt(prompt: Prompt, promptProvider: suspend (GetPromptRequest) -> GetPromptResult) { - if (capabilities.prompts == null) { + public fun addPrompt( + prompt: Prompt, + promptProvider: suspend ClientConnection.(GetPromptRequest) -> GetPromptResult, + ) { + checkNotNull(options.capabilities.prompts) { logger.error { "Failed to add prompt '${prompt.name}': Server does not support prompts capability" } - throw IllegalStateException("Server does not support prompts capability.") + "Server does not support prompts capability." } - logger.info { "Registering prompt: ${prompt.name}" } - _prompts.update { current -> current.put(prompt.name, RegisteredPrompt(prompt, promptProvider)) } + promptRegistry.add(RegisteredPrompt(prompt, promptProvider)) } /** @@ -323,7 +432,7 @@ public open class Server(private val serverInfo: Implementation, options: Server name: String, description: String? = null, arguments: List? = null, - promptProvider: suspend (GetPromptRequest) -> GetPromptResult, + promptProvider: suspend ClientConnection.(GetPromptRequest) -> GetPromptResult, ) { val prompt = Prompt(name = name, description = description, arguments = arguments) addPrompt(prompt, promptProvider) @@ -336,12 +445,11 @@ public open class Server(private val serverInfo: Implementation, options: Server * @throws IllegalStateException If the server does not support prompts. */ public fun addPrompts(promptsToAdd: List) { - if (capabilities.prompts == null) { + checkNotNull(options.capabilities.prompts) { logger.error { "Failed to add prompts: Server does not support prompts capability" } - throw IllegalStateException("Server does not support prompts capability.") + "Server does not support prompts capability." } - logger.info { "Registering ${promptsToAdd.size} prompts" } - _prompts.update { current -> current.putAll(promptsToAdd.associateBy { it.prompt.name }) } + promptRegistry.addAll(promptsToAdd) } /** @@ -352,23 +460,12 @@ public open class Server(private val serverInfo: Implementation, options: Server * @throws IllegalStateException If the server does not support prompts. */ public fun removePrompt(name: String): Boolean { - if (capabilities.prompts == null) { + checkNotNull(options.capabilities.prompts) { logger.error { "Failed to remove prompt '$name': Server does not support prompts capability" } - throw IllegalStateException("Server does not support prompts capability.") + "Server does not support prompts capability." } - logger.info { "Removing prompt: $name" } - val oldMap = _prompts.getAndUpdate { current -> current.remove(name) } - - val removed = name in oldMap - logger.debug { - if (removed) { - "Prompt removed: $name" - } else { - "Prompt not found: $name" - } - } - return removed + return promptRegistry.remove(name) } /** @@ -379,24 +476,12 @@ public open class Server(private val serverInfo: Implementation, options: Server * @throws IllegalStateException If the server does not support prompts. */ public fun removePrompts(promptNames: List): Int { - if (capabilities.prompts == null) { + checkNotNull(options.capabilities.prompts) { logger.error { "Failed to remove prompts: Server does not support prompts capability" } - throw IllegalStateException("Server does not support prompts capability.") + "Server does not support prompts capability." } - logger.info { "Removing ${promptNames.size} prompts" } - val oldMap = _prompts.getAndUpdate { current -> current - promptNames.toPersistentSet() } - - val removedCount = promptNames.count { it in oldMap } - - logger.info { - if (removedCount > 0) { - "Removed $removedCount prompts" - } else { - "No prompts were removed" - } - } - return removedCount + return promptRegistry.removeAll(promptNames) } /** @@ -414,19 +499,14 @@ public open class Server(private val serverInfo: Implementation, options: Server name: String, description: String, mimeType: String = "text/html", - readHandler: suspend (ReadResourceRequest) -> ReadResourceResult, + readHandler: suspend ClientConnection.(ReadResourceRequest) -> ReadResourceResult, ) { - if (capabilities.resources == null) { + checkNotNull(options.capabilities.resources) { logger.error { "Failed to add resource '$name': Server does not support resources capability" } - throw IllegalStateException("Server does not support resources capability.") - } - logger.info { "Registering resource: $name ($uri)" } - _resources.update { current -> - current.put( - uri, - RegisteredResource(Resource(uri, name, description, mimeType), readHandler), - ) + "Server does not support resources capability." } + val resource = Resource(uri, name, description, mimeType) + resourceRegistry.add(RegisteredResource(resource, readHandler)) } /** @@ -436,12 +516,11 @@ public open class Server(private val serverInfo: Implementation, options: Server * @throws IllegalStateException If the server does not support resources. */ public fun addResources(resourcesToAdd: List) { - if (capabilities.resources == null) { + checkNotNull(options.capabilities.resources) { logger.error { "Failed to add resources: Server does not support resources capability" } - throw IllegalStateException("Server does not support resources capability.") + "Server does not support resources capability." } - logger.info { "Registering ${resourcesToAdd.size} resources" } - _resources.update { current -> current.putAll(resourcesToAdd.associateBy { it.resource.uri }) } + resourceRegistry.addAll(resourcesToAdd) } /** @@ -452,23 +531,11 @@ public open class Server(private val serverInfo: Implementation, options: Server * @throws IllegalStateException If the server does not support resources. */ public fun removeResource(uri: String): Boolean { - if (capabilities.resources == null) { + checkNotNull(options.capabilities.resources) { logger.error { "Failed to remove resource '$uri': Server does not support resources capability" } - throw IllegalStateException("Server does not support resources capability.") + "Server does not support resources capability." } - logger.info { "Removing resource: $uri" } - - val oldMap = _resources.getAndUpdate { current -> current.remove(uri) } - - val removed = uri in oldMap - logger.debug { - if (removed) { - "Resource removed: $uri" - } else { - "Resource not found: $uri" - } - } - return removed + return resourceRegistry.remove(uri) } /** @@ -479,362 +546,365 @@ public open class Server(private val serverInfo: Implementation, options: Server * @throws IllegalStateException If the server does not support resources. */ public fun removeResources(uris: List): Int { - if (capabilities.resources == null) { + checkNotNull(options.capabilities.resources) { logger.error { "Failed to remove resources: Server does not support resources capability" } - throw IllegalStateException("Server does not support resources capability.") + "Server does not support resources capability." } - logger.info { "Removing ${uris.size} resources" } - - val oldMap = _resources.getAndUpdate { current -> current - uris.toPersistentSet() } - - val removedCount = uris.count { it in oldMap } - - logger.info { - if (removedCount > 0) { - "Removed $removedCount resources" - } else { - "No resources were removed" - } - } - return removedCount + return resourceRegistry.removeAll(uris) } /** - * Sends a ping request to the client to check connectivity. + * Registers a resource template. Clients can discover it via `resources/templates/list` + * and read matching URIs via `resources/read`. * - * @return The result of the ping request. - * @throws IllegalStateException If for some reason the method is not supported or the connection is closed. + * @param template The [ResourceTemplate] describing the URI template pattern. + * @param readHandler A suspend function invoked when a client reads a URI that matches + * the template. The second parameter contains the URI variables extracted from the match. + * @throws IllegalStateException If the server does not support resources. */ - public suspend fun ping(): EmptyRequestResult = request(PingRequest()) + public fun addResourceTemplate( + template: ResourceTemplate, + readHandler: suspend ClientConnection.(ReadResourceRequest, Map) -> ReadResourceResult, + ) { + checkNotNull(options.capabilities.resources) { + "Server does not support resources capability." + } + val matcher = options.resourceTemplateMatcherFactory.create(template) + resourceTemplateRegistry.add( + RegisteredResourceTemplate( + template, + matcher, + readHandler = readHandler, + ), + ) + } /** - * Creates a message using the server's sampling capability. + * Registers a resource template by constructing a [ResourceTemplate] from given parameters. * + * @param uriTemplate The RFC 6570 URI template string (e.g. `"file:///{path}"`). + * @param name A human-readable name for the template. + * @param description A human-readable description of the resource template. + * @param mimeType The MIME type of resource content served by this template. + * @param readHandler A suspend function invoked when a client reads a URI that matches + * the template. The second parameter contains the URI variables extracted from the match. + * @throws IllegalStateException If the server does not support resources. + */ + public fun addResourceTemplate( + uriTemplate: String, + name: String, + description: String? = null, + mimeType: String? = null, + readHandler: suspend ClientConnection.(ReadResourceRequest, Map) -> ReadResourceResult, + ) { + addResourceTemplate(ResourceTemplate(uriTemplate, name, description, mimeType), readHandler) + } + + /** + * Removes a resource template by its URI template string. + * + * @param uriTemplate The URI template string identifying the template to remove. + * @return True if the template was removed, false if it was not found. + * @throws IllegalStateException If the server does not support resources. + */ + public fun removeResourceTemplate(uriTemplate: String): Boolean { + checkNotNull(options.capabilities.resources) { + "Server does not support resources capability." + } + return resourceTemplateRegistry.remove(uriTemplate) + } + + // --- Internal Handlers --- + private fun handleSubscribeResources(session: ServerSession, request: SubscribeRequest) { + if (options.capabilities.resources?.subscribe ?: false) { + logger.debug { "Subscribing to resources" } + notificationService.subscribeToResourceUpdate(session, request.params.uri) + } else { + logger.debug { "Failed to subscribe to resources: Server does not support resources capability" } + } + } + + private fun handleUnsubscribeResources(session: ServerSession, request: UnsubscribeRequest) { + if (options.capabilities.resources?.subscribe ?: false) { + logger.debug { "Unsubscribing from resources" } + notificationService.unsubscribeFromResourceUpdate(session, request.params.uri) + } else { + logger.debug { "Failed to unsubscribe from resources: Server does not support resources capability" } + } + } + + private fun handleListTools(): ListToolsResult { + val toolList = tools.values.map { it.tool } + return ListToolsResult(tools = toolList, nextCursor = null) + } + + private suspend fun handleCallTool(session: ServerSession, request: CallToolRequest): CallToolResult { + val requestParams = request.params + logger.debug { "Handling tool call request for tool: ${requestParams.name}" } + + // Check if the tool exists + val tool = toolRegistry.get(requestParams.name) ?: run { + logger.error { "Tool not found: ${requestParams.name}" } + return CallToolResult( + content = listOf(TextContent(text = "Tool ${requestParams.name} not found")), + isError = true, + ) + } + + // Execute the tool handler and catch any errors + return try { + logger.trace { "Executing tool ${requestParams.name} with input: ${requestParams.arguments}" } + tool.run { + session.clientConnection.handler(request) + } + } catch (e: CancellationException) { + throw e + } catch (e: UrlElicitationRequiredException) { + // Surface a required URL-mode elicitation as a JSON-RPC error (-32042) + throw e + } catch (e: Exception) { + logger.error(e) { "Error executing tool ${requestParams.name}" } + CallToolResult( + content = listOf(TextContent(text = "Error executing tool ${requestParams.name}: ${e.message}")), + isError = true, + ) + } + } + + private fun handleListPrompts(): ListPromptsResult { + logger.debug { "Handling list prompts request" } + return ListPromptsResult(prompts = prompts.values.map { it.prompt }) + } + + private suspend fun handleGetPrompt(session: ServerSession, request: GetPromptRequest): GetPromptResult { + val requestParams = request.params + logger.debug { "Handling get prompt request for: ${requestParams.name}" } + val prompt = promptRegistry.get(requestParams.name) + ?: run { + logger.error { "Prompt not found: ${requestParams.name}" } + throw McpException( + code = RPCError.ErrorCode.INVALID_PARAMS, + message = "Prompt not found: ${requestParams.name}", + ) + } + return prompt.run { + session.clientConnection.messageProvider(request) + } + } + + private fun handleListResources(): ListResourcesResult { + logger.debug { "Handling list resources request" } + return ListResourcesResult(resources = resources.values.map { it.resource }) + } + + private suspend fun handleReadResource(session: ServerSession, request: ReadResourceRequest): ReadResourceResult { + val uri = request.params.uri + logger.debug { "Handling read resource request for: $uri" } + + // Priority 1: exact URI match + resourceRegistry.get(uri)?.let { resource -> + return resource.run { session.clientConnection.readHandler(request) } + } + + // Priority 2: most-specific matching template. + // Selection: highest score wins; on tie, fewest variables wins; on tie, registration order wins. + val (template, matchResult) = resourceTemplateRegistry.values.values + .mapNotNull { tmpl -> tmpl.matcher.match(uri)?.let { tmpl to it } } + .maxWithOrNull( + compareBy> { (_, result) -> result.score } + .thenByDescending { (_, result) -> result.variables.size }, + ) + ?: run { + logger.error { "Resource not found: $uri" } + throw McpException( + code = RPCError.ErrorCode.RESOURCE_NOT_FOUND, + message = "Resource not found", + data = buildJsonObject { put("uri", uri) }, + ) + } + + logger.debug { "Matched resource template '${template.key}' for URI: $uri" } + return template.run { session.clientConnection.readHandler(request, matchResult.variables) } + } + + private fun handleListResourceTemplates(): ListResourceTemplatesResult { + logger.debug { "Handling list resource templates request" } + return ListResourceTemplatesResult(resourceTemplateRegistry.values.values.map { it.resourceTemplate }) + } + + // Start the ServerSession / ClientConnection redirection section + + /** Returns the [ClientConnection] for the session identified by [sessionId]. */ + public fun clientConnection(sessionId: String): ClientConnection = + sessionRegistry.getSession(sessionId).clientConnection + + /** + * Triggers [ClientConnection.ping] request for session by provided [sessionId]. + * @param sessionId The session ID to ping + */ + public suspend fun ping(sessionId: String): EmptyResult = clientConnection(sessionId).ping() + + /** + * Triggers [ClientConnection.createMessage] request for session by provided [sessionId]. + * + * @param sessionId The session ID to create a message. * @param params The parameters for creating a message. * @param options Optional request options. * @return The created message result. * @throws IllegalStateException If the server does not support sampling or if the request fails. */ public suspend fun createMessage( + sessionId: String, params: CreateMessageRequest, options: RequestOptions? = null, - ): CreateMessageResult { - logger.debug { "Creating message with params: $params" } - return request(params, options) - } + ): CreateMessageResult = clientConnection(sessionId).createMessage(params, options) /** - * Lists the available "roots" from the client's perspective (if supported). + * Triggers [ClientConnection.listRoots] request for session by provided [sessionId]. * + * @param sessionId The session ID to list roots for. * @param params JSON parameters for the request, usually empty. * @param options Optional request options. * @return The list of roots. * @throws IllegalStateException If the server or client does not support roots. */ public suspend fun listRoots( + sessionId: String, params: JsonObject = EmptyJsonObject, options: RequestOptions? = null, - ): ListRootsResult { - logger.debug { "Listing roots with params: $params" } - return request(ListRootsRequest(params), options) - } + ): ListRootsResult = sessionRegistry.getSession(sessionId).listRoots(params, options) + /** + * Triggers [ClientConnection.createElicitation] request for session by provided [sessionId]. + * + * @param sessionId The session ID to create elicitation for. + * @param message The elicitation message. + * @param requestedSchema The requested schema for the elicitation. + * @param options Optional request options. + * @return The created elicitation result. + * @throws IllegalStateException If the server does not support elicitation or if the request fails. + */ public suspend fun createElicitation( + sessionId: String, message: String, - requestedSchema: RequestedSchema, + requestedSchema: ElicitRequestParams.RequestedSchema, options: RequestOptions? = null, - ): CreateElicitationResult { - logger.debug { "Creating elicitation with message: $message" } - return request(CreateElicitationRequest(message, requestedSchema), options) - } + ): ElicitResult = clientConnection(sessionId).createElicitation( + message = message, + requestedSchema = requestedSchema, + options = options, + ) /** - * Sends a logging message notification to the client. + * Triggers URL mode [ClientConnection.createElicitation] request for session by provided [sessionId]. * - * @param params The logging message notification parameters. + * @param sessionId The session ID to create elicitation for. + * @param message The message explaining why the interaction is needed. + * @param elicitationId A unique identifier for the elicitation. + * @param url The URL that the user should navigate to. + * @param options Optional request options. + * @return The created elicitation result. + * @throws IllegalStateException If the server does not support elicitation or if the request fails. */ - public suspend fun sendLoggingMessage(params: LoggingMessageNotification) { - logger.trace { "Sending logging message: ${params.params.data}" } - notification(params) - } + public suspend fun createElicitation( + sessionId: String, + message: String, + elicitationId: String, + url: String, + options: RequestOptions? = null, + ): ElicitResult = clientConnection(sessionId).createElicitation( + message = message, + elicitationId = elicitationId, + url = url, + options = options, + ) /** - * Sends a resource-updated notification to the client, indicating that a specific resource has changed. + * Triggers [ClientConnection.sendLoggingMessage] for session by provided [sessionId]. * - * @param params Details of the updated resource. + * @param sessionId The session ID to send the logging message to. + * @param notification The logging message notification. */ - public suspend fun sendResourceUpdated(params: ResourceUpdatedNotification) { - logger.debug { "Sending resource updated notification for: ${params.params.uri}" } - notification(params) + public suspend fun sendLoggingMessage(sessionId: String, notification: LoggingMessageNotification) { + clientConnection(sessionId).sendLoggingMessage(notification) } /** - * Sends a notification to the client indicating that the list of resources has changed. - */ - public suspend fun sendResourceListChanged() { - logger.debug { "Sending resource list changed notification" } - notification(ResourceListChangedNotification()) - } - - /** - * Sends a notification to the client indicating that the list of tools has changed. - */ - public suspend fun sendToolListChanged() { - logger.debug { "Sending tool list changed notification" } - notification(ToolListChangedNotification()) - } - - /** - * Sends a notification to the client indicating that the list of prompts has changed. - */ - public suspend fun sendPromptListChanged() { - logger.debug { "Sending prompt list changed notification" } - notification(PromptListChangedNotification()) - } - - // --- Internal Handlers --- - - private suspend fun handleInitialize(request: InitializeRequest): InitializeResult { - logger.info { "Handling initialize request from client ${request.clientInfo}" } - clientCapabilities = request.capabilities - clientVersion = request.clientInfo - - val requestedVersion = request.protocolVersion - val protocolVersion = if (SUPPORTED_PROTOCOL_VERSIONS.contains(requestedVersion)) { - requestedVersion - } else { - logger.warn { - "Client requested unsupported protocol version $requestedVersion, falling back to $LATEST_PROTOCOL_VERSION" - } - LATEST_PROTOCOL_VERSION - } - - return InitializeResult( - protocolVersion = protocolVersion, - capabilities = capabilities, - serverInfo = serverInfo, - ) - } - - private suspend fun handleListTools(): ListToolsResult { - val toolList = tools.values.map { it.tool } - return ListToolsResult(tools = toolList, nextCursor = null) - } - - private suspend fun handleCallTool(request: CallToolRequest): CallToolResult { - logger.debug { "Handling tool call request for tool: ${request.name}" } - val tool = _tools.value[request.name] - ?: run { - logger.error { "Tool not found: ${request.name}" } - throw IllegalArgumentException("Tool not found: ${request.name}") - } - logger.trace { "Executing tool ${request.name} with input: ${request.arguments}" } - return tool.handler(request) - } - - private suspend fun handleListPrompts(): ListPromptsResult { - logger.debug { "Handling list prompts request" } - return ListPromptsResult(prompts = prompts.values.map { it.prompt }) - } - - private suspend fun handleGetPrompt(request: GetPromptRequest): GetPromptResult { - logger.debug { "Handling get prompt request for: ${request.name}" } - val prompt = prompts[request.name] - ?: run { - logger.error { "Prompt not found: ${request.name}" } - throw IllegalArgumentException("Prompt not found: ${request.name}") - } - return prompt.messageProvider(request) - } - - private suspend fun handleListResources(): ListResourcesResult { - logger.debug { "Handling list resources request" } - return ListResourcesResult(resources = resources.values.map { it.resource }) - } - - private suspend fun handleReadResource(request: ReadResourceRequest): ReadResourceResult { - logger.debug { "Handling read resource request for: ${request.uri}" } - val resource = resources[request.uri] - ?: run { - logger.error { "Resource not found: ${request.uri}" } - throw IllegalArgumentException("Resource not found: ${request.uri}") - } - return resource.readHandler(request) - } - - private suspend fun handleListResourceTemplates(): ListResourceTemplatesResult { - // If you have resource templates, return them here. For now, return empty. - return ListResourceTemplatesResult(listOf()) - } - - /** - * Asserts that the client supports the capability required for the given [method]. + * Triggers [ClientConnection.sendResourceUpdated] for session by provided [sessionId]. * - * This method is automatically called by the [Protocol] framework before handling requests. - * Throws [IllegalStateException] if the capability is not supported. - * - * @param method The method for which we are asserting capability. + * @param sessionId The session ID to send the resource updated notification to. + * @param notification Details of the updated resource. */ - override fun assertCapabilityForMethod(method: Method) { - logger.trace { "Asserting capability for method: ${method.value}" } - when (method.value) { - "sampling/createMessage" -> { - if (clientCapabilities?.sampling == null) { - logger.error { "Client capability assertion failed: sampling not supported" } - throw IllegalStateException("Client does not support sampling (required for ${method.value})") - } - } + public suspend fun sendResourceUpdated(sessionId: String, notification: ResourceUpdatedNotification) { + clientConnection(sessionId).sendResourceUpdated(notification) + } - "roots/list" -> { - if (clientCapabilities?.roots == null) { - throw IllegalStateException("Client does not support listing roots (required for ${method.value})") - } - } + /** + * Triggers [ClientConnection.sendResourceListChanged] for session by provided [sessionId]. + * + * @param sessionId The session ID to send the resource list changed notification to. + */ + public suspend fun sendResourceListChanged(sessionId: String) { + clientConnection(sessionId).sendResourceListChanged() + } - "elicitation/create" -> { - if (clientCapabilities?.elicitation == null) { - throw IllegalStateException("Client does not support elicitation (required for ${method.value})") - } - } + /** + * Triggers [ClientConnection.sendToolListChanged] for session by provided [sessionId]. + * + * @param sessionId The session ID to send the tool list changed notification to. + */ + public suspend fun sendToolListChanged(sessionId: String) { + clientConnection(sessionId).sendToolListChanged() + } - "ping" -> { - // No specific capability required - } + /** + * Triggers [ClientConnection.sendPromptListChanged] for session by provided [sessionId]. + * + * @param sessionId The session ID to send the prompt list changed notification to. + */ + public suspend fun sendPromptListChanged(sessionId: String) { + clientConnection(sessionId).sendPromptListChanged() + } + + /** + * Triggers [ClientConnection.sendElicitationComplete] for session by provided [sessionId]. + * + * @param sessionId The session ID to send the elicitation complete notification to. + * @param notification Details of the completed elicitation. + */ + public suspend fun sendElicitationComplete(sessionId: String, notification: ElicitationCompleteNotification) { + clientConnection(sessionId).sendElicitationComplete(notification) + } + // End the ServerSession / ClientConnection redirection section + + // Start the notification handling section + + /** Registers a notification [handler] for the given [method] on all active sessions. */ + public fun setNotificationHandler(method: Method, handler: (notification: T) -> Deferred) { + sessions.forEach { (_, session) -> + session.setNotificationHandler(method, handler) } } - /** - * Asserts that the server can handle the specified notification method. - * - * Throws [IllegalStateException] if the server does not have the capabilities required to handle this notification. - * - * @param method The notification method. - */ - override fun assertNotificationCapability(method: Method) { - logger.trace { "Asserting notification capability for method: ${method.value}" } - when (method.value) { - "notifications/message" -> { - if (capabilities.logging == null) { - logger.error { "Server capability assertion failed: logging not supported" } - throw IllegalStateException("Server does not support logging (required for ${method.value})") - } - } - - "notifications/resources/updated", - "notifications/resources/list_changed", - -> { - if (capabilities.resources == null) { - throw IllegalStateException( - "Server does not support notifying about resources (required for ${method.value})", - ) - } - } - - "notifications/tools/list_changed" -> { - if (capabilities.tools == null) { - throw IllegalStateException( - "Server does not support notifying of tool list changes (required for ${method.value})", - ) - } - } - - "notifications/prompts/list_changed" -> { - if (capabilities.prompts == null) { - throw IllegalStateException( - "Server does not support notifying of prompt list changes (required for ${method.value})", - ) - } - } - - "notifications/cancelled", - "notifications/progress", - -> { - // Always allowed - } + /** Removes the notification handler for the given [method] from all active sessions. */ + public fun removeNotificationHandler(method: Method) { + sessions.forEach { (_, session) -> + session.removeNotificationHandler(method) } } - /** - * Asserts that the server can handle the specified request method. - * - * Throws [IllegalStateException] if the server does not have the capabilities required to handle this request. - * - * @param method The request method. - */ - override fun assertRequestHandlerCapability(method: Method) { - logger.trace { "Asserting request handler capability for method: ${method.value}" } - when (method.value) { - "sampling/createMessage" -> { - if (capabilities.sampling == null) { - logger.error { "Server capability assertion failed: sampling not supported" } - throw IllegalStateException("Server does not support sampling (required for $method)") - } - } - - "logging/setLevel" -> { - if (capabilities.logging == null) { - throw IllegalStateException("Server does not support logging (required for $method)") - } - } - - "prompts/get", - "prompts/list", - -> { - if (capabilities.prompts == null) { - throw IllegalStateException("Server does not support prompts (required for $method)") - } - } - - "resources/list", - "resources/templates/list", - "resources/read", - -> { - if (capabilities.resources == null) { - throw IllegalStateException("Server does not support resources (required for $method)") - } - } - - "tools/call", - "tools/list", - -> { - if (capabilities.tools == null) { - throw IllegalStateException("Server does not support tools (required for $method)") - } - } - - "ping", "initialize" -> { - // No capability required - } - } + /** Registers a notification [handler] for the given [method] on a specific session. */ + public fun setNotificationHandler( + sessionId: String, + method: Method, + handler: (notification: T) -> Deferred, + ) { + sessionRegistry.getSessionOrNull(sessionId)?.setNotificationHandler(method, handler) } + + /** Removes the notification handler for the given [method] from a specific session. */ + public fun removeNotificationHandler(sessionId: String, method: Method) { + sessionRegistry.getSessionOrNull(sessionId)?.removeNotificationHandler(method) + } + // End the notification handling section } - -/** - * A wrapper class representing a registered tool on the server. - * - * @property tool The tool definition. - * @property handler A suspend function to handle the tool call requests. - */ -public data class RegisteredTool(val tool: Tool, val handler: suspend (CallToolRequest) -> CallToolResult) - -/** - * A wrapper class representing a registered prompt on the server. - * - * @property prompt The prompt definition. - * @property messageProvider A suspend function that returns the prompt content when requested by the client. - */ -public data class RegisteredPrompt( - val prompt: Prompt, - val messageProvider: suspend (GetPromptRequest) -> GetPromptResult, -) - -/** - * A wrapper class representing a registered resource on the server. - * - * @property resource The resource definition. - * @property readHandler A suspend function to handle read requests for this resource. - */ -public data class RegisteredResource( - val resource: Resource, - val readHandler: suspend (ReadResourceRequest) -> ReadResourceResult, -) diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt new file mode 100644 index 0000000..f20d3e1 --- /dev/null +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt @@ -0,0 +1,501 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.github.oshai.kotlinlogging.KotlinLogging +import io.modelcontextprotocol.kotlin.sdk.shared.Protocol +import io.modelcontextprotocol.kotlin.sdk.shared.RequestOptions +import io.modelcontextprotocol.kotlin.sdk.types.BaseRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageResult +import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.ElicitResult +import io.modelcontextprotocol.kotlin.sdk.types.ElicitationCompleteNotification +import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject +import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest +import io.modelcontextprotocol.kotlin.sdk.types.InitializeResult +import io.modelcontextprotocol.kotlin.sdk.types.InitializedNotification +import io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION +import io.modelcontextprotocol.kotlin.sdk.types.ListRootsRequest +import io.modelcontextprotocol.kotlin.sdk.types.ListRootsResult +import io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel +import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.Method.Defined +import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import io.modelcontextprotocol.kotlin.sdk.types.RequestMeta +import io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotification +import io.modelcontextprotocol.kotlin.sdk.types.SUPPORTED_PROTOCOL_VERSIONS +import io.modelcontextprotocol.kotlin.sdk.types.SetLevelRequest +import kotlinx.atomicfu.AtomicRef +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.CompletableDeferred +import kotlinx.serialization.json.JsonObject +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +private val logger = KotlinLogging.logger {} + +/** + * Represents an active server-side session in the Model Context Protocol. + * + * Owns the server side of a single client connection on top of [Protocol]. Drives the + * initialize/initialized handshake, tracks the negotiated client capabilities and version, + * asserts capability requirements before dispatching requests and notifications, and exposes + * server-to-client operations such as sampling, root listing, elicitation, logging, and + * list-change notifications. + * + * Equality and hashing are based on [sessionId], so a session can be used as a key in + * routing structures. + * + * @property serverInfo server name and version reported to the client during initialization + * @param options server capabilities and protocol options used by [Protocol] + * @property instructions optional human-readable instructions for the client, sent in the initialize result + */ +public open class ServerSession( + protected val serverInfo: Implementation, + options: ServerOptions, + protected val instructions: String?, +) : Protocol(options) { + + /** Unique identifier for this session, generated on creation. */ + @OptIn(ExperimentalUuidApi::class) + public val sessionId: String = Uuid.random().toString() + + private var _onInitialized: (() -> Unit) = {} + + private var _onClose: () -> Unit = {} + + private val _clientCapabilities: AtomicRef = atomic(null) + private val _clientVersion: AtomicRef = atomic(null) + + /** Capabilities reported by the client during initialization, or `null` before the handshake completes. */ + public val clientCapabilities: ClientCapabilities? get() = _clientCapabilities.value + + /** Client implementation information reported during initialization, or `null` before the handshake completes. */ + public val clientVersion: Implementation? get() = _clientVersion.value + + /** + * The capabilities supported by the server, related to the session. + */ + private val serverCapabilities = options.capabilities + + /** + * The current logging level set by the client. + * When null, all messages are sent (no filtering). + */ + internal val currentLoggingLevel: AtomicRef = atomic(null) + + init { + // Core protocol handlers + setRequestHandler(Defined.Initialize) { request, _ -> + handleInitialize(request) + } + setNotificationHandler(Defined.NotificationsInitialized) { + _onInitialized() + CompletableDeferred(Unit) + } + + // Logging level handler + if (options.capabilities.logging != null) { + setRequestHandler(Defined.LoggingSetLevel) { request, _ -> + currentLoggingLevel.value = request.params.level + logger.debug { "Logging level set to: ${request.params.level}" } + EmptyResult() + } + } + } + + override fun onInitializedNotification() { + // Enable concurrency before the user's onInitialized callback, which runs synchronously. + enableConcurrentDispatch() + } + + internal val clientConnection: ClientConnection = ClientConnectionImpl(this) + + /** + * Registers a callback to be invoked when the server has completed initialization. + * + * The callback must be synchronous and fast: it runs on the message-dispatch path for + * `notifications/initialized`, after concurrent dispatch has been enabled for the session. + */ + public fun onInitialized(block: () -> Unit) { + val old = _onInitialized + _onInitialized = { + old() + block() + } + } + + /** + * Registers a callback to be invoked when the server session is closing. + */ + public fun onClose(block: () -> Unit) { + val old = _onClose + _onClose = { + old() + block() + } + } + + /** + * Called when the server session is closing. + */ + override fun onClose() { + logger.debug { "Server connection closing" } + _onClose() + } + + /** + * Asserts that the client supports the capability required for the given [method]. + * + * This method is automatically called by the [Protocol] framework before handling requests. + * Throws [IllegalStateException] if the capability is not supported. + * + * @param method The method for which we are asserting capability. + */ + override fun assertCapabilityForMethod(method: Method) { + logger.trace { "Asserting capability for method: ${method.value}" } + when (method) { + Defined.SamplingCreateMessage -> { + if (clientCapabilities?.sampling == null) { + logger.error { "Client capability assertion failed: sampling not supported" } + error("Client does not support sampling (required for ${method.value})") + } + } + + Defined.RootsList -> { + if (clientCapabilities?.roots == null) { + logger.error { "Client capability assertion failed: listing roots not supported" } + error("Client does not support listing roots (required for ${method.value})") + } + } + + Defined.ElicitationCreate -> { + if (clientCapabilities?.elicitation == null) { + logger.error { "Client capability assertion failed: elicitation not supported" } + error("Client does not support elicitation (required for ${method.value})") + } + } + + Defined.TasksGet, + Defined.TasksResult, + Defined.TasksList, + Defined.TasksCancel, + -> assertTasksCapabilityForMethod(method) + + Defined.Ping -> { + // No specific capability required + } + + else -> { + // For notifications not specifically listed, no assertion by default + } + } + } + + private fun assertTasksCapabilityForMethod(method: Method) { + val tasks = clientCapabilities?.tasks + ?: error("Client does not support tasks (required for ${method.value})") + when (method) { + Defined.TasksList -> { + if (tasks.list == null) { + error("Client does not support listing tasks (required for ${method.value})") + } + } + + Defined.TasksCancel -> { + if (tasks.cancel == null) { + error("Client does not support cancelling tasks (required for ${method.value})") + } + } + + else -> { + // TasksGet, TasksResult: base tasks capability suffices. + } + } + } + + /** + * Asserts that the server can handle the specified notification method. + * + * Throws [IllegalStateException] if the server does not have the capabilities required to handle this notification. + * + * @param method The notification method. + */ + override fun assertNotificationCapability(method: Method) { + logger.trace { "Asserting notification capability for method: ${method.value}" } + when (method) { + Defined.NotificationsMessage -> { + if (serverCapabilities.logging == null) { + logger.error { "Server capability assertion failed: logging not supported" } + error("Server does not support logging (required for ${method.value})") + } + } + + Defined.NotificationsResourcesUpdated, + Defined.NotificationsResourcesListChanged, + -> { + if (serverCapabilities.resources == null) { + error( + "Server does not support notifying about resources (required for ${method.value})", + ) + } + } + + Defined.NotificationsToolsListChanged -> { + if (serverCapabilities.tools == null) { + error( + "Server does not support notifying of tool list changes (required for ${method.value})", + ) + } + } + + Defined.NotificationsPromptsListChanged -> { + if (serverCapabilities.prompts == null) { + error( + "Server does not support notifying of prompt list changes (required for ${method.value})", + ) + } + } + + Defined.NotificationsTasksStatus -> { + if (serverCapabilities.tasks == null) { + error("Server does not support tasks (required for ${method.value})") + } + } + + Defined.NotificationsCancelled, + Defined.NotificationsProgress, + -> { + // Always allowed + } + + else -> { + // For notifications not specifically listed, no assertion by default + } + } + } + + /** + * Asserts that the server can handle the specified request method. + * + * Throws [IllegalStateException] if the server does not have the capabilities required to handle this request. + * + * @param method The request method. + */ + override fun assertRequestHandlerCapability(method: Method) { + logger.trace { "Asserting request handler capability for method: ${method.value}" } + when (method) { + Defined.SamplingCreateMessage -> { + if (serverCapabilities.experimental?.get("sampling") == null) { + logger.error { "Server capability assertion failed: sampling not supported" } + error("Server does not support sampling (required for $method)") + } + } + + Defined.LoggingSetLevel -> { + if (serverCapabilities.logging == null) { + logger.error { "Server does not support logging (required for $method)" } + error("Server does not support logging (required for $method)") + } + } + + Defined.PromptsGet, + Defined.PromptsList, + -> { + if (serverCapabilities.prompts == null) { + error("Server does not support prompts (required for $method)") + } + } + + Defined.ResourcesList, + Defined.ResourcesTemplatesList, + Defined.ResourcesRead, + Defined.ResourcesSubscribe, + Defined.ResourcesUnsubscribe, + -> { + if (serverCapabilities.resources == null) { + error("Server does not support resources (required for $method)") + } + } + + Defined.ToolsCall, + Defined.ToolsList, + -> { + if (serverCapabilities.tools == null) { + error("Server does not support tools (required for $method)") + } + } + + Defined.Ping, Defined.Initialize -> { + // No capability required + } + + else -> { + // For notifications not specifically listed, no assertion by default + } + } + } + + private fun handleInitialize(request: InitializeRequest): InitializeResult { + if (!_clientCapabilities.compareAndSet(null, request.params.capabilities)) { + throw McpException( + code = RPCError.ErrorCode.INVALID_REQUEST, + message = "Server already initialized", + ) + } + + logger.debug { "Handling initialization request from client" } + _clientVersion.value = request.params.clientInfo + + val requestedVersion = request.params.protocolVersion + val protocolVersion = if (SUPPORTED_PROTOCOL_VERSIONS.contains(requestedVersion)) { + requestedVersion + } else { + logger.warn { + "Client requested unsupported protocol version $requestedVersion, " + + "falling back to $LATEST_PROTOCOL_VERSION" + } + LATEST_PROTOCOL_VERSION + } + + return InitializeResult( + protocolVersion = protocolVersion, + capabilities = serverCapabilities, + serverInfo = serverInfo, + instructions = instructions, + ) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ServerSession) return false + return sessionId == other.sessionId + } + + override fun hashCode(): Int = sessionId.hashCode() + + // Start the ClientConnection redirection section + + /** + * Sends a ping request to the client to check connectivity. + * + * @return The result of the ping request. + * @throws IllegalStateException If for some reason the method is not supported or the connection is closed. + */ + public suspend fun ping(): EmptyResult = clientConnection.ping() + + /** + * Creates a message using the server's sampling capability. + * + * @param params The parameters for creating a message. + * @param options Optional request options. + * @return The created message result. + * @throws IllegalStateException If the server does not support sampling or if the request fails. + */ + public suspend fun createMessage( + params: CreateMessageRequest, + options: RequestOptions? = null, + ): CreateMessageResult = clientConnection.createMessage(params, options) + + /** + * Lists the available "roots" from the client's perspective (if supported). + * + * @param params JSON parameters for the request, usually empty. + * @param options Optional request options. + * @return The list of roots. + * @throws IllegalStateException If the server or client does not support roots. + */ + public suspend fun listRoots( + params: JsonObject = EmptyJsonObject, + options: RequestOptions? = null, + ): ListRootsResult { + logger.debug { "Listing roots with params: $params" } + return request(ListRootsRequest(BaseRequestParams(RequestMeta(params))), options) + } + + /** + * Sends a message to the client requesting an elicitation. + * This typically results in a form being displayed to the end user. + * + * Accepted content is validated against [requestedSchema]. If content is absent, an empty + * object is used for validation only, so declared `required` properties still apply. The + * returned [ElicitResult] is not modified. + * + * @param message The message for the elicitation to display. + * @param requestedSchema The schema the server requests for the elicitation result. + * Influences the form displayed to the user. + * @param options Optional request options. + * @return The result of the elicitation request. + * @throws IllegalStateException If the server or client does not support elicitation. + * @throws McpException If an accepted response does not match [requestedSchema]. + */ + public suspend fun createElicitation( + message: String, + requestedSchema: ElicitRequestParams.RequestedSchema, + options: RequestOptions? = null, + ): ElicitResult = clientConnection.createElicitation(message, requestedSchema, options) + + /** + * Sends a URL mode elicitation request to the client, directing the user + * to an external URL for out-of-band interactions. + * + * @param message The message explaining why the interaction is needed. + * @param elicitationId A unique identifier for the elicitation. + * @param url The URL that the user should navigate to. + * @param options Optional request options. + * @return The result of the elicitation request. + * @throws IllegalStateException If the server or client does not support elicitation. + */ + public suspend fun createElicitation( + message: String, + elicitationId: String, + url: String, + options: RequestOptions? = null, + ): ElicitResult = clientConnection.createElicitation(message, elicitationId, url, options) + + /** + * Sends a logging message notification to the client. + * Messages are filtered based on the current logging level set by the client. + * If no logging level is set, all messages are sent. + * + * @param notification The logging message notification. + */ + public suspend fun sendLoggingMessage(notification: LoggingMessageNotification): Unit = + clientConnection.sendLoggingMessage(notification) + + /** + * Sends a resource-updated notification to the client, indicating that a specific resource has changed. + * + * @param notification Details of the updated resource. + */ + public suspend fun sendResourceUpdated(notification: ResourceUpdatedNotification): Unit = + clientConnection.sendResourceUpdated(notification) + + /** + * Sends a notification to the client indicating that the list of resources has changed. + */ + public suspend fun sendResourceListChanged(): Unit = clientConnection.sendResourceListChanged() + + /** + * Sends a notification to the client indicating that the list of tools has changed. + */ + public suspend fun sendToolListChanged(): Unit = clientConnection.sendToolListChanged() + + /** + * Sends a notification to the client indicating that the list of prompts has changed. + */ + public suspend fun sendPromptListChanged(): Unit = clientConnection.sendPromptListChanged() + + /** + * Sends a notification to the client indicating that an out-of-band elicitation has completed. + * + * @param notification Details of the completed elicitation. + */ + public suspend fun sendElicitationComplete(notification: ElicitationCompleteNotification): Unit = + clientConnection.sendElicitationComplete(notification) + // End the ClientConnection redirection section +} diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSessionRegistry.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSessionRegistry.kt new file mode 100644 index 0000000..57980d3 --- /dev/null +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSessionRegistry.kt @@ -0,0 +1,60 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.github.oshai.kotlinlogging.KotlinLogging +import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.update +import kotlinx.collections.immutable.persistentMapOf + +internal typealias ServerSessionKey = String + +/** + * Represents a registry for managing server sessions. + */ +internal class ServerSessionRegistry { + + private val logger = KotlinLogging.logger {} + + /** + * Atomic variable used to maintain a thread-safe registry of sessions. + * Stores a persistent map where each session is identified by its unique key. + */ + private val registry = atomic(persistentMapOf()) + + /** + * Returns a read-only view of the current server sessions. + */ + internal val sessions: Map + get() = registry.value + + /** + * Returns a server session by its ID. + * @param sessionId The ID of the session to retrieve. + * @throws IllegalArgumentException If the session doesn't exist. + */ + internal fun getSession(sessionId: ServerSessionKey): ServerSession = + sessions[sessionId] ?: throw IllegalArgumentException("Session not found: $sessionId") + + /** + * Returns a server session by its ID, or null if it doesn't exist. + * @param sessionId The ID of the session to retrieve. + */ + internal fun getSessionOrNull(sessionId: ServerSessionKey): ServerSession? = sessions[sessionId] + + /** + * Registers a server session. + * @param session The session to register. + */ + internal fun addSession(session: ServerSession) { + logger.info { "Adding session: ${session.sessionId}" } + registry.update { sessions -> sessions.put(session.sessionId, session) } + } + + /** + * Removes a server session by its ID. + * @param sessionId The ID of the session to remove. + */ + internal fun removeSession(sessionId: ServerSessionKey) { + logger.info { "Removing session: $sessionId" } + registry.update { sessions -> sessions.remove(sessionId) } + } +} diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt index 0fa18b8..9261be2 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt @@ -1,17 +1,30 @@ package io.modelcontextprotocol.kotlin.sdk.server import io.github.oshai.kotlinlogging.KotlinLogging -import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage import io.modelcontextprotocol.kotlin.sdk.internal.IODispatcher import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport import io.modelcontextprotocol.kotlin.sdk.shared.ReadBuffer +import io.modelcontextprotocol.kotlin.sdk.shared.TooLongFrameException +import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions import io.modelcontextprotocol.kotlin.sdk.shared.serializeMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.McpDsl +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ClosedSendChannelException +import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -21,131 +34,347 @@ import kotlinx.io.Source import kotlinx.io.buffered import kotlinx.io.readByteArray import kotlinx.io.writeString -import kotlin.concurrent.atomics.AtomicBoolean +import kotlin.concurrent.atomics.AtomicReference import kotlin.concurrent.atomics.ExperimentalAtomicApi -import kotlin.coroutines.CoroutineContext + +private const val READ_BUFFER_SIZE = 8192L /** - * A server transport that communicates with a client via standard I/O. + * Server-side MCP transport that exchanges JSON-RPC messages over arbitrary byte streams. * - * Reads from System.in and writes to System.out. + * Reads framed messages from the supplied [Source] and writes framed messages to the supplied + * [Sink]. Three internal coroutines drive the pipeline: + * + * - **reader** — pulls bytes from the input source into the parsing buffer; runs on + * [Builder.ioDispatcher]. + * - **processor** — parses messages out of the buffer and invokes the registered message handler; + * runs on [Builder.handlerDispatcher] (defaults to [Dispatchers.Default]) so blocking handler + * code does not starve the I/O pool. + * - **writer** — serialises outbound messages and flushes them to the output sink; runs on + * [Builder.ioDispatcher]. + * + * Both internal channels are bounded, so a slow handler or slow output naturally back-pressures + * the upstream producer — [send] suspends when the outbound channel is full. + * + * Both explicit [close] and a natural EOF from the input perform a graceful drain: in-flight + * outbound messages are flushed before `onClose` fires, and the input source and output sink are + * released. + * + * Example: + * ```kotlin + * val transport = StdioServerTransport( + * input = System.`in`.asSource().buffered(), + * output = System.out.asSink().buffered(), + * ) { + * scope = myScope + * } + * transport.start() + * ``` */ @OptIn(ExperimentalAtomicApi::class) -public class StdioServerTransport(private val inputStream: Source, outputStream: Sink) : AbstractTransport() { +public class StdioServerTransport private constructor( + private val input: Source, + output: Sink, + private val scope: CoroutineScope?, + private val handlerDispatcher: CoroutineDispatcher, + private val ioDispatcher: CoroutineDispatcher, +) : AbstractTransport() { + + /** + * Creates a [StdioServerTransport] reading from [input] and writing to [output], with + * optional configuration applied through a [Builder] block. + * + * @param input source the transport reads JSON-RPC messages from + * @param output sink the transport writes JSON-RPC messages to + * @param block configuration applied to the underlying [Builder] + */ + public constructor( + input: Source, + output: Sink, + block: Builder.() -> Unit = {}, + ) : this(Builder(input, output).apply(block)) + + private constructor(builder: Builder) : this( + input = builder.input, + output = builder.output, + scope = builder.scope, + handlerDispatcher = builder.handlerDispatcher, + ioDispatcher = builder.ioDispatcher, + ) + + /** + * Creates a [StdioServerTransport] from the given input source and output sink with default + * configuration. Retained for binary compatibility; prefer the [Builder]-based constructor. + * + * @param inputStream source the transport reads JSON-RPC messages from + * @param outputStream sink the transport writes JSON-RPC messages to + */ + @Deprecated( + message = "Use StdioServerTransport(input, output) { ... } instead.", + replaceWith = ReplaceWith("StdioServerTransport(input = inputStream, output = outputStream)"), + level = DeprecationLevel.WARNING, + ) + public constructor(inputStream: Source, outputStream: Sink) : this( + input = inputStream, + output = outputStream, + scope = null, + handlerDispatcher = Dispatchers.Default, + ioDispatcher = IODispatcher.limitedParallelism(2), + ) + private val logger = KotlinLogging.logger {} - private val readBuffer = ReadBuffer() - private val initialized: AtomicBoolean = AtomicBoolean(false) - private var readingJob: Job? = null - private var sendingJob: Job? = null + private val readChannel = Channel(Channel.BUFFERED) + private val writeChannel = Channel(Channel.BUFFERED) + private val outputSink = output.buffered() - private val coroutineContext: CoroutineContext = IODispatcher + SupervisorJob() - private val scope = CoroutineScope(coroutineContext) - private val readChannel = Channel(Channel.UNLIMITED) - private val writeChannel = Channel(Channel.UNLIMITED) - private val outputWriter = outputStream.buffered() + private enum class State { New, Operational, Stopped } + private val state: AtomicReference = AtomicReference(State.New) + + private var readerJob: Job? = null + private var processorJob: Job? = null + private var writerJob: Job? = null + + private var effectiveScope: CoroutineScope? = null + private val ownsScope: Boolean = (scope == null) + + private val setupComplete = CompletableDeferred() + + /** + * Starts the reader, processor, and writer coroutines. Must be called exactly once before + * messages can be exchanged; subsequent calls throw. + */ override suspend fun start() { - if (!initialized.compareAndSet(expectedValue = false, newValue = true)) { - error("StdioServerTransport already started!") - } - - // Launch a coroutine to read from stdin - readingJob = scope.launch { - val buf = Buffer() - try { - while (isActive) { - val bytesRead = inputStream.readAtMostTo(buf, 8192) - if (bytesRead == -1L) { - // EOF reached - break - } - if (bytesRead > 0) { - val chunk = buf.readByteArray() - readChannel.send(chunk) - } - } - } catch (e: Throwable) { - logger.error(e) { "Error reading from stdin" } - _onError.invoke(e) - } finally { - // Reached EOF or error, close connection - close() + if (!state.compareAndSet(State.New, State.Operational)) { + when (state.load()) { + State.Stopped -> error("StdioServerTransport is already closed!") + else -> error("StdioServerTransport already started!") } } - // Launch a coroutine to process messages from readChannel - scope.launch { - try { - for (chunk in readChannel) { - readBuffer.append(chunk) - processReadBuffer() - } - } catch (e: Throwable) { - _onError.invoke(e) - } - } + try { + val resolvedScope = scope ?: CoroutineScope( + currentCoroutineContext() + IODispatcher + SupervisorJob(), + ) + effectiveScope = resolvedScope - // Launch a coroutine to handle message sending - sendingJob = scope.launch { - try { - for (message in writeChannel) { - val json = serializeMessage(message) - outputWriter.writeString(json) - outputWriter.flush() - } - } catch (e: Throwable) { - logger.error(e) { "Error writing to stdout" } - _onError.invoke(e) - } + readerJob = resolvedScope.launch( + ioDispatcher + CoroutineName("StdioServerTransport.reader"), + ) { readerPump() } + processorJob = resolvedScope.launch( + handlerDispatcher + CoroutineName("StdioServerTransport.processor"), + start = CoroutineStart.UNDISPATCHED, + ) { processorPump() } + writerJob = resolvedScope.launch( + ioDispatcher + CoroutineName("StdioServerTransport.writer"), + start = CoroutineStart.UNDISPATCHED, + ) { writerPump() } + } finally { + setupComplete.complete(Unit) } } - private suspend fun processReadBuffer() { - while (true) { - val message = try { - readBuffer.readMessage() - } catch (e: Throwable) { - _onError.invoke(e) - null + private suspend fun readerPump() { + val buf = Buffer() + try { + while (currentCoroutineContext().isActive) { + val bytesRead = input.readAtMostTo(buf, READ_BUFFER_SIZE) + if (bytesRead == -1L) break + if (bytesRead > 0) { + val chunk = buf.readByteArray() + readChannel.send(chunk) + } } - - if (message == null) break - // Async invocation broke delivery order - try { - _onMessage.invoke(message) - } catch (e: Throwable) { - _onError.invoke(e) + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + if (state.load() == State.Stopped) { + logger.debug(e) { "Reader interrupted by close()" } + } else { + logger.error(e) { "Error reading from input source" } + _onError(e) } + } finally { + readChannel.close() } } + private suspend fun processorPump() { + try { + for (chunk in readChannel) { + readBuffer.append(chunk) + while (true) { + val message = try { + readBuffer.readMessage() + } catch (e: CancellationException) { + throw e + } catch (e: TooLongFrameException) { + throw e + } catch (e: Throwable) { + _onError(e) + null + } ?: break + try { + _onMessage(message) + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + logger.error(e) { "Error processing message" } + _onError(e) + } + } + } + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + _onError(e) + } finally { + writeChannel.close() + } + } + + private suspend fun writerPump() { + try { + for (message in writeChannel) { + val json = serializeMessage(message) + outputSink.writeString(json) + outputSink.flush() + } + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + logger.error(e) { "Error writing to output sink" } + _onError(e) + } finally { + transitionToStoppedNaturally() + } + } + + /** + * Closes the transport. When called after [start], waits for in-flight outbound messages to be + * flushed, releases the input source and output sink, cancels the internal scope when the + * transport owns it, and invokes `onClose`. When called before [start], transitions directly + * to the closed state without releasing the input/output or invoking `onClose` — the caller + * remains responsible for resources that were never handed off to a running transport. Safe to + * call multiple times and safe to race with [start]. + */ override suspend fun close() { - if (!initialized.compareAndSet(expectedValue = true, newValue = false)) return + var previous: State + do { + previous = state.load() + } while (previous != State.Stopped && !state.compareAndSet(previous, State.Stopped)) + + if (previous == State.New) { + setupComplete.complete(Unit) + return + } withContext(NonCancellable) { - writeChannel.close() - sendingJob?.cancelAndJoin() + setupComplete.await() - runCatching { - inputStream.close() - }.onFailure { logger.warn(it) { "Failed to close stdin" } } - - readingJob?.cancel() + if (previous == State.Stopped) { + writerJob?.join() + return@withContext + } + runCatching { input.close() } + .onFailure { logger.warn(it) { "Failed to close input source" } } + readerJob?.cancel() readChannel.close() + processorJob?.cancelAndJoin() + writeChannel.close() + writerJob?.join() + runCatching { outputSink.close() } + .onFailure { logger.warn(it) { "Failed to close output sink" } } readBuffer.clear() - - runCatching { - outputWriter.flush() - outputWriter.close() - }.onFailure { logger.warn(it) { "Failed to close stdout" } } - - _onClose.invoke() + if (ownsScope) { + effectiveScope?.coroutineContext?.get(Job)?.cancel() + } + invokeOnCloseCallback() } } - override suspend fun send(message: JSONRPCMessage) { - writeChannel.send(message) + /** + * Queues [message] for the writer coroutine. Suspends when the outbound channel is full, + * applying back-pressure to the caller. Throws [McpException] with + * [RPCError.ErrorCode.CONNECTION_CLOSED] if the transport has not been started or has + * already closed. + * + * @param message JSON-RPC message to send + * @param options transport-specific send options; ignored by this transport + */ + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { + when (state.load()) { + State.New -> throw McpException( + code = RPCError.ErrorCode.CONNECTION_CLOSED, + message = "Transport is not started", + ) + + State.Stopped -> throw McpException( + code = RPCError.ErrorCode.CONNECTION_CLOSED, + message = "Transport is closed", + ) + + State.Operational -> Unit + } + try { + writeChannel.send(message) + } catch (e: CancellationException) { + throw e + } catch (e: ClosedSendChannelException) { + throw McpException( + code = RPCError.ErrorCode.CONNECTION_CLOSED, + message = "Transport is closed", + cause = e, + ) + } + } + + private fun transitionToStoppedNaturally() { + if (!state.compareAndSet(State.Operational, State.Stopped)) return + runCatching { input.close() } + .onFailure { logger.warn(it) { "Failed to close input source" } } + runCatching { outputSink.close() } + .onFailure { logger.warn(it) { "Failed to close output sink" } } + readBuffer.clear() + if (ownsScope) { + // Non-null in practice: writer launches after effectiveScope is published. + effectiveScope?.coroutineContext?.get(Job)?.cancel() + } + invokeOnCloseCallback() + } + + /** + * Configuration builder for [StdioServerTransport]. Used via the + * `StdioServerTransport(input, output) { ... }` factory; the I/O endpoints are supplied + * positionally, while [scope], [handlerDispatcher], and [ioDispatcher] are configurable + * inside the block. + * + * Example: + * ```kotlin + * StdioServerTransport(input, output) { + * scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + * handlerDispatcher = myHandlerDispatcher + * ioDispatcher = Dispatchers.IO + * } + * ``` + * + * @property scope Optional caller-supplied [CoroutineScope]. When non-null, the transport's + * pipeline coroutines run as children of this scope and the transport does not tear it + * down on [close]. When `null`, the transport creates and owns an internal scope. + * @property handlerDispatcher Dispatcher for invoking the registered message handler. + * Defaults to [Dispatchers.Default]. + * @property ioDispatcher Dispatcher for the reader and writer coroutines. Must allow at + * least two threads to run concurrently so the reader and writer don't block each other. + * The default is a two-thread view of the platform I/O dispatcher + * (`IODispatcher.limitedParallelism(2)`); pass a different value to share or isolate I/O + * threads with the rest of your application. + */ + @McpDsl + public class Builder internal constructor(internal val input: Source, internal val output: Sink) { + public var scope: CoroutineScope? = null + public var handlerDispatcher: CoroutineDispatcher = Dispatchers.Default + public var ioDispatcher: CoroutineDispatcher = IODispatcher.limitedParallelism(2) } } diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt new file mode 100644 index 0000000..dcba848 --- /dev/null +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt @@ -0,0 +1,950 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.ktor.http.BadContentTypeFormatException +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpMethod +import io.ktor.http.HttpStatusCode +import io.ktor.http.parseHeaderValue +import io.ktor.server.application.ApplicationCall +import io.ktor.server.request.contentType +import io.ktor.server.request.header +import io.ktor.server.request.httpMethod +import io.ktor.server.response.header +import io.ktor.server.response.respond +import io.ktor.server.response.respondText +import io.ktor.server.sse.ServerSSESession +import io.ktor.util.collections.ConcurrentMap +import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport +import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions +import io.modelcontextprotocol.kotlin.sdk.types.CancelledNotificationParams +import io.modelcontextprotocol.kotlin.sdk.types.DEFAULT_NEGOTIATED_PROTOCOL_VERSION +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCEmptyMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCError +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse +import io.modelcontextprotocol.kotlin.sdk.types.McpJson +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.REQUEST_TIMEOUT +import io.modelcontextprotocol.kotlin.sdk.types.RequestId +import io.modelcontextprotocol.kotlin.sdk.types.SUPPORTED_PROTOCOL_VERSIONS +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.job +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.decodeFromJsonElement +import kotlin.concurrent.atomics.AtomicBoolean +import kotlin.concurrent.atomics.ExperimentalAtomicApi +import kotlin.coroutines.cancellation.CancellationException +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +internal const val MCP_SESSION_ID_HEADER = "mcp-session-id" +private const val MCP_PROTOCOL_VERSION_HEADER = "mcp-protocol-version" +private const val MCP_RESUMPTION_TOKEN_HEADER = "Last-Event-ID" +private const val MIN_PRIMING_EVENT_PROTOCOL_VERSION = "2025-11-25" + +/** + * A holder for an active request call. + * If [StreamableHttpServerTransport.Configuration.enableJsonResponse] is true, the session is null. + * Otherwise, the session is not null. + */ +private data class SessionContext(val session: ServerSSESession?, val call: ApplicationCall) + +/** + * Server transport for Streamable HTTP: this implements the MCP Streamable HTTP transport specification. + * It supports both SSE streaming and direct HTTP responses. + * + * In stateful mode: + * - Session ID is generated and included in response headers + * - Session ID is always included in initialization responses + * - Requests with invalid session IDs are rejected with 404 Not Found + * - Non-initialization requests without a session ID are rejected with 400 Bad Request + * - State is maintained in-memory (connections, message history) + * + * In stateless mode: + * - No Session ID is included in any responses + * - No session validation is performed + * + * @param configuration Transport configuration. See [Configuration] for available options. + */ +@OptIn(ExperimentalUuidApi::class, ExperimentalAtomicApi::class) +public class StreamableHttpServerTransport(private val configuration: Configuration) : AbstractTransport() { + + @Deprecated("Use default constructor with explicit Configuration()") + public constructor() : this(configuration = Configuration()) + + /** + * Secondary constructor for `StreamableHttpServerTransport` that simplifies initialization by directly taking the + * configurable parameters without requiring a `Configuration` instance. + * + * @param enableJsonResponse Determines whether the server should return JSON responses. + * Defaults to `false`. + * @param enableDnsRebindingProtection Enables DNS rebinding protection. + * Defaults to `false`. + * @param allowedHosts A list of hosts allowed for server communication. + * Defaults to `null`, allowing all hosts. + * @param allowedOrigins A list of allowed origins for CORS (Cross-Origin Resource Sharing). + * Defaults to `null`, allowing all origins. + * @param eventStore The `EventStore` instance for handling resumable events. + * Defaults to `null`, disabling resumability. + * @param retryIntervalMillis Retry interval in milliseconds for event handling or reconnection attempts. + * Defaults to `null`. + */ + @Deprecated( + "Use constructor with Configuration: StreamableHttpServerTransport(Configuration(enableJsonResponse = ...))", + replaceWith = ReplaceWith( + "StreamableHttpServerTransport(Configuration(enableJsonResponse = enableJsonResponse, enableDnsRebindingProtection = enableDnsRebindingProtection, allowedHosts = allowedHosts, allowedOrigins = allowedOrigins, eventStore = eventStore, retryIntervalMillis = retryIntervalMillis))", + ), + ) + public constructor( + enableJsonResponse: Boolean = false, + enableDnsRebindingProtection: Boolean = false, + allowedHosts: List? = null, + allowedOrigins: List? = null, + eventStore: EventStore? = null, + retryIntervalMillis: Long? = null, + ) : this( + Configuration( + enableJsonResponse = enableJsonResponse, + enableDnsRebindingProtection = enableDnsRebindingProtection, + allowedHosts = allowedHosts, + allowedOrigins = allowedOrigins, + eventStore = eventStore, + retryInterval = retryIntervalMillis?.milliseconds, + ), + ) + + /** + * Configuration options for [StreamableHttpServerTransport]. + * + * @property enableJsonResponse when `true`, returns direct JSON responses instead of SSE streams + * @property enableDnsRebindingProtection enables DNS rebinding protection + * @property allowedHosts list of hosts allowed for server communication, or `null` to allow all + * @property allowedOrigins list of allowed CORS origins, or `null` to allow all + * @property eventStore store for resumable events, or `null` to disable resumability + * @property retryInterval retry interval for SSE reconnection attempts + * @property maxRequestBodySize Maximum allowed size (in bytes) for incoming request bodies. + * Defaults to 4 MB (4,194,304 bytes). + */ + public class Configuration( + public val enableJsonResponse: Boolean = false, + @Deprecated( + message = "Use install(DnsRebindingProtection) on your Ktor route instead", + level = DeprecationLevel.WARNING, + ) + public val enableDnsRebindingProtection: Boolean = false, + @Deprecated( + message = "Use install(DnsRebindingProtection) on your Ktor route instead", + level = DeprecationLevel.WARNING, + ) + public val allowedHosts: List? = null, + @Deprecated( + message = "Use install(DnsRebindingProtection) on your Ktor route instead", + level = DeprecationLevel.WARNING, + ) + public val allowedOrigins: List? = null, + public val eventStore: EventStore? = null, + public val retryInterval: Duration? = null, + public val maxRequestBodySize: Long = DEFAULT_MAX_REQUEST_BODY_SIZE, + ) { + init { + require(maxRequestBodySize > 0) { + "maxRequestBodySize must be greater than 0" + } + } + } + + /** Session identifier assigned after initialization in stateful mode, or `null` in stateless mode. */ + public var sessionId: String? = null + private set + + private var sessionIdGenerator: (() -> String)? = { Uuid.random().toString() } + private var onSessionInitialized: ((sessionId: String) -> Unit)? = null + private var onSessionClosed: ((sessionId: String) -> Unit)? = null + + private val started: AtomicBoolean = AtomicBoolean(false) + private val initialized: AtomicBoolean = AtomicBoolean(false) + + private val streamsMapping: ConcurrentMap = ConcurrentMap() + private val requestToStreamMapping: ConcurrentMap = ConcurrentMap() + private val requestToResponseMapping: ConcurrentMap = ConcurrentMap() + + /** + * JSON-response mode only: one entry per in-flight POST, completed by [send] with the collected + * responses once every request delivered by that POST has been answered. [handlePostRequest] + * awaits it so the HTTP exchange stays open while handlers run concurrently. + */ + private val pendingJsonResponses: ConcurrentMap>> = ConcurrentMap() + + /** + * JSON-response mode only: request ids retired by an inbound cancellation while their POST is + * still awaiting. Treated as settled-without-response so the POST's "a response for every id" + * completion condition is not blocked by a request that will never be answered. + */ + private val cancelledRequestIds: ConcurrentMap = ConcurrentMap() + + private val sessionMutex = Mutex() + private val streamMutex = Mutex() + + private companion object { + const val STANDALONE_SSE_STREAM_ID = "_GET_stream" + } + + /** + * Function that generates a session ID for the transport. + * The session ID SHOULD be globally unique and cryptographically secure + * (e.g., a securely generated UUID, a JWT, or a cryptographic hash) + * + * Set undefined to disable session management. + */ + public fun setSessionIdGenerator(block: (() -> String)?) { + sessionIdGenerator = block + } + + /** + * A callback for session initialization events + * This is called when the server initializes a new session. + * Useful in cases when you need to register multiple mcp sessions + * and need to keep track of them. + */ + public fun setOnSessionInitialized(block: ((String) -> Unit)?) { + onSessionInitialized = block + } + + /** + * A callback for session close events + * This is called when the server closes a session due to a DELETE request. + * Useful in cases when you need to clean up resources associated with the session. + * Note that this is different from the transport closing, if you are handling + * HTTP requests from multiple nodes you might want to close each + * StreamableHTTPServerTransport after a request is completed while still keeping the + * session open/running. + */ + public fun setOnSessionClosed(block: ((String) -> Unit)?) { + onSessionClosed = block + } + + override suspend fun start() { + check(started.compareAndSet(expectedValue = false, newValue = true)) { + "StreamableHttpServerTransport already started! If using Server class, " + + "note that connect() calls start() automatically." + } + } + + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { + val responseRequestId: RequestId? = when (message) { + is JSONRPCResponse -> message.id + is JSONRPCError -> message.id + else -> null + } + val routingRequestId = responseRequestId ?: options?.relatedRequestId + + // Standalone SSE stream + if (routingRequestId == null) { + require(message !is JSONRPCResponse && message !is JSONRPCError) { + "Cannot send a response on a standalone SSE stream unless resuming a previous client request" + } + val standaloneStream = streamsMapping[STANDALONE_SSE_STREAM_ID] ?: return + emitOnStream(STANDALONE_SSE_STREAM_ID, standaloneStream.session, message) + return + } + + val isTerminated = message is JSONRPCResponse || message is JSONRPCError + + // In JSON mode request-related notifications have no per-request SSE stream. Route them to the + // standalone GET stream before the lookup below, so a notification for an already-retired + // request is delivered there instead of becoming a spurious error. + if (configuration.enableJsonResponse && !isTerminated) { + val standaloneStream = streamsMapping[STANDALONE_SSE_STREAM_ID] + standaloneStream?.let { emitOnStream(STANDALONE_SSE_STREAM_ID, it.session, message) } + return + } + + val streamId = requestToStreamMapping[routingRequestId] + if (streamId == null) { + // Request already retired (completed, cancelled, or the JSON POST disconnected). In JSON + // mode a late terminal response is undeliverable and dropped. In SSE mode an unknown id + // is a real routing error. + if (configuration.enableJsonResponse) return + error("No connection established for request id $routingRequestId") + } + val activeStream = streamsMapping[streamId] + + if (!configuration.enableJsonResponse) { + activeStream?.let { stream -> + emitOnStream(streamId, stream.session, message) + } + if (!isTerminated) return + } + + requestToResponseMapping[responseRequestId!!] = message + + streamMutex.withLock { + // The request may have been retired (disconnect or cancellation cleanup) between recording + // the response above and acquiring the lock. Reclaim the stray mapping and drop it. + if (routingRequestId !in requestToStreamMapping) { + requestToResponseMapping.remove(routingRequestId) + return + } + // Recomputed under the lock. Handlers may respond concurrently, and the sender that + // observes the last settled request also retires the ids, so a losing sender sees an + // empty set. A cancelled request counts as settled even though it produces no response. + val relatedIds = requestToStreamMapping.filterValues { it == streamId }.keys + if (relatedIds.isEmpty() || + relatedIds.any { it !in requestToResponseMapping && it !in cancelledRequestIds } + ) { + return + } + + if (configuration.enableJsonResponse) { + // Hand the collected responses to the POST suspended in [handlePostRequest], which + // completes the HTTP exchange. A null pendingPost means the POST was already retired + // (disconnect or cancellation), so the responses are undeliverable and dropped quietly. + val pendingPost = pendingJsonResponses.remove(streamId) + if (pendingPost == null) { + cleanUpRequests(relatedIds) + return + } + val responses = relatedIds.mapNotNull { requestToResponseMapping[it] } + cleanUpRequests(relatedIds) + pendingPost.complete(responses) + } else { + // A null stream means the per-request SSE connection closed before the response + // settled, so the ids are retired rather than leaked. + activeStream?.session?.close() + cleanUpRequests(relatedIds) + } + } + } + + private fun cleanUpRequests(ids: Collection) { + ids.forEach { requestId -> + requestToResponseMapping.remove(requestId) + requestToStreamMapping.remove(requestId) + cancelledRequestIds.remove(requestId) + } + } + + /** + * Scans a delivered POST body for `notifications/cancelled` and retires each referenced request + * that is awaiting on a JSON-mode POST. No-op unless [Configuration.enableJsonResponse]. + */ + private suspend fun retireCancelledJsonRequests(messages: List) { + if (!configuration.enableJsonResponse) return + val cancelledIds = messages.asSequence() + .filterIsInstance() + .filter { it.method == Method.Defined.NotificationsCancelled.value } + .mapNotNull { notification -> + val params = notification.params ?: return@mapNotNull null + runCatching { + McpJson.decodeFromJsonElement(params).requestId + }.getOrNull() + } + .toList() + cancelledIds.forEach { retireCancelledJsonRequest(it) } + } + + /** + * Marks a request cancelled by the peer as settled without a response and completes its POST once + * every request that POST delivered is settled (answered or cancelled). Without this, a cancelled + * request leaves the POST awaiting a response that, by design, is never sent. + */ + private suspend fun retireCancelledJsonRequest(requestId: RequestId) { + streamMutex.withLock { + val streamId = requestToStreamMapping[requestId] ?: return + cancelledRequestIds[requestId] = Unit + val relatedIds = requestToStreamMapping.filterValues { it == streamId }.keys + val allSettled = relatedIds.all { it in requestToResponseMapping || it in cancelledRequestIds } + if (!allSettled) return + val pendingPost = pendingJsonResponses.remove(streamId) ?: return + val responses = relatedIds.mapNotNull { requestToResponseMapping[it] } + cleanUpRequests(relatedIds) + pendingPost.complete(responses) + } + } + + override suspend fun close() { + withContext(NonCancellable) { + streamMutex.withLock { + streamsMapping.values.forEach { + try { + it.session?.close() + } catch (_: Exception) { + } + } + streamsMapping.clear() + requestToStreamMapping.clear() + requestToResponseMapping.clear() + cancelledRequestIds.clear() + // Release any POST calls suspended awaiting JSON responses. + pendingJsonResponses.values.forEach { it.cancel() } + pendingJsonResponses.clear() + invokeOnCloseCallback() + } + } + } + + /** + * Handles an incoming HTTP request, whether GET, POST or DELETE. + * + * **Note:** this method does not perform DNS rebinding protection on its own. + * When using this transport outside the [mcpStreamableHttp] / [mcpStatelessStreamableHttp] + * DSL, install the [DnsRebindingProtection] plugin on your Ktor route + * (or apply equivalent middleware) to validate `Host` / `Origin` headers. + */ + public suspend fun handleRequest(session: ServerSSESession?, call: ApplicationCall) { + validateHeaders(call)?.let { reason -> + call.reject(HttpStatusCode.Forbidden, RPCError.ErrorCode.CONNECTION_CLOSED, reason) + _onError(Error(reason)) + return + } + + when (call.request.httpMethod) { + HttpMethod.Post -> handlePostRequest(session, call) + + HttpMethod.Get -> handleGetRequest(session, call) + + HttpMethod.Delete -> handleDeleteRequest(call) + + else -> call.run { + response.header(HttpHeaders.Allow, "GET, POST, DELETE") + reject(HttpStatusCode.MethodNotAllowed, RPCError.ErrorCode.CONNECTION_CLOSED, "Method not allowed.") + } + } + } + + /** + * Handles POST requests containing JSON-RPC messages. + * + * In JSON-response mode this suspends until a response has been produced for every request in + * the POST body, then completes the HTTP exchange with the collected JSON (a single object for + * one request, an array for a batch). Bodies without requests are acknowledged with 202 + * immediately. In SSE mode the responses are emitted onto the per-request SSE stream instead. + */ + public suspend fun handlePostRequest(session: ServerSSESession?, call: ApplicationCall) { + try { + if (!configuration.enableJsonResponse && session == null) { + error("Server session can't be null for SSE responses") + } + + val acceptHeader = call.request.header(HttpHeaders.Accept) + val isAcceptEventStream = acceptHeader.accepts(ContentType.Text.EventStream) + val isAcceptJson = acceptHeader.accepts(ContentType.Application.Json) + + if (!isAcceptEventStream || !isAcceptJson) { + call.reject( + HttpStatusCode.NotAcceptable, + RPCError.ErrorCode.CONNECTION_CLOSED, + "Not Acceptable: Client must accept both application/json and text/event-stream", + ) + return + } + + if (!call.request.contentType().match(ContentType.Application.Json)) { + call.reject( + HttpStatusCode.UnsupportedMediaType, + RPCError.ErrorCode.CONNECTION_CLOSED, + "Unsupported Media Type: Content-Type must be application/json", + ) + return + } + + val messages = parseBody(call) ?: return + val initializationRequest = messages.filterIsInstance().firstOrNull { + it.method == Method.Defined.Initialize.value + } + val isInitializationRequest = initializationRequest != null + + if (isInitializationRequest) { + if (initialized.load() && sessionId != null) { + call.reject( + HttpStatusCode.BadRequest, + RPCError.ErrorCode.INVALID_REQUEST, + "Invalid Request: Server already initialized", + initializationRequest.id, + ) + return + } + if (!validateProtocolVersion(call, initializationRequest.id)) return + if (messages.size > 1) { + call.reject( + HttpStatusCode.BadRequest, + RPCError.ErrorCode.INVALID_REQUEST, + "Invalid Request: Only one initialization request is allowed", + initializationRequest.id, + ) + return + } + + sessionMutex.withLock { + if (sessionId != null) return@withLock + sessionId = sessionIdGenerator?.invoke() + initialized.store(true) + sessionId?.let { onSessionInitialized?.invoke(it) } + } + } else { + val requestId = messages.filterIsInstance().firstOrNull()?.id + if (!validateSession(call, requestId) || !validateProtocolVersion(call, requestId)) return + } + + val hasRequest = messages.any { it is JSONRPCRequest } + if (!hasRequest) { + call.respond(HttpStatusCode.Accepted) + messages.forEach { message -> _onMessage(message) } + // A cancellation may target a request still awaiting delivery on a JSON-mode POST; + // retire it so that POST completes instead of waiting for a response that never comes. + retireCancelledJsonRequests(messages) + return + } + + // Extract protocol version for priming event decision. + // For initialize requests, get from request params. + // For other requests, get from header (already validated). + val clientProtocolVersion = if (isInitializationRequest) { + (initializationRequest.params as? JsonObject)?.get("protocolVersion") + ?.let { McpJson.decodeFromJsonElement(it) } + ?: DEFAULT_NEGOTIATED_PROTOCOL_VERSION + } else { + call.request.header(MCP_PROTOCOL_VERSION_HEADER) ?: DEFAULT_NEGOTIATED_PROTOCOL_VERSION + } + + val streamId = Uuid.random().toString() + if (!configuration.enableJsonResponse) { + call.appendSseHeaders() + flushSse(session) // flush headers immediately + maybeSendPrimingEvent(streamId, session, clientProtocolVersion) + } + + val pendingPost = if (configuration.enableJsonResponse) { + CompletableDeferred>() + } else { + null + } + + streamMutex.withLock { + streamsMapping[streamId] = SessionContext(session, call) + messages.filterIsInstance().forEach { requestToStreamMapping[it.id] = streamId } + pendingPost?.let { pendingJsonResponses[streamId] = it } + } + call.coroutineContext.job.invokeOnCompletion { streamsMapping.remove(streamId) } + + if (pendingPost == null) { + // SSE mode: responses are emitted onto this call's SSE stream by send(). + messages.forEach { message -> _onMessage(message) } + return + } + + // JSON-response mode. Handlers may run concurrently, so returning from delivery no longer + // implies the responses were produced. Suspend until send() has collected a response for + // every request in this POST. A client disconnect or transport close cancels this call. + try { + messages.forEach { message -> _onMessage(message) } + retireCancelledJsonRequests(messages) + val responses = pendingPost.await() + sessionId?.let { call.response.header(MCP_SESSION_ID_HEADER, it) } + if (responses.isEmpty()) { + // Every request in this POST was cancelled before producing a response; there is + // nothing to return, so acknowledge with 202 rather than an empty JSON body. + call.respond(HttpStatusCode.Accepted) + } else { + call.response.header(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + val payload = if (responses.size == 1) responses.first() else responses + call.respond(payload) + } + } finally { + // On cancellation or failure, retire this POST's ids so a late response is dropped + // quietly at the send() boundary instead of leaking mappings. Idempotent no-ops on + // the success path (send() or retirement already cleaned up). + pendingJsonResponses.remove(streamId) + pendingPost.cancel() + cleanUpRequests(messages.filterIsInstance().map { it.id }) + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + call.reject( + HttpStatusCode.BadRequest, + RPCError.ErrorCode.PARSE_ERROR, + "Parse error: ${e.message}", + ) + _onError(e) + } + } + + /** Handles an HTTP GET request by establishing a standalone SSE stream for server-initiated notifications. */ + public suspend fun handleGetRequest(session: ServerSSESession?, call: ApplicationCall) { + // NOTE: enableJsonResponse only controls how POST responses are delivered (JSON vs. SSE). + // The standalone GET SSE stream is always supported — it is the only channel available + // for server-to-client notifications when enableJsonResponse = true. + val sseSession = session ?: error("Server session can't be null for streaming GET requests") + + val acceptHeader = call.request.header(HttpHeaders.Accept) + if (!acceptHeader.accepts(ContentType.Text.EventStream)) { + call.reject( + HttpStatusCode.NotAcceptable, + RPCError.ErrorCode.CONNECTION_CLOSED, + "Not Acceptable: Client must accept text/event-stream", + ) + return + } + + if (!validateSession(call) || !validateProtocolVersion(call)) return + + configuration.eventStore?.let { store -> + call.request.header(MCP_RESUMPTION_TOKEN_HEADER)?.let { lastEventId -> + replayEvents(store, lastEventId, sseSession) + return + } + } + + // SSE headers (Content-Type, Cache-Control, Connection) are already set by the framework's SSE handler + flushSse(sseSession) + val newContext = SessionContext(sseSession, call) + streamMutex.withLock { + streamsMapping[STANDALONE_SSE_STREAM_ID]?.let { existingContext -> + // Close the previous SSE session. If alive, this cancels the old + // coroutine (which will hit its identity-guarded finally — that finally + // won't double-remove, since we replace the mapping below). + try { + existingContext.session?.close() + } catch (e: CancellationException) { + throw e + } catch (_: Exception) { + // Ignore — the old stream may already be closed. + } + // Evict the stale mapping — the old session is closed either way. + streamsMapping.remove(STANDALONE_SSE_STREAM_ID) + } + streamsMapping[STANDALONE_SSE_STREAM_ID] = newContext + } + val clientProtocolVersion = + call.request.header(MCP_PROTOCOL_VERSION_HEADER) ?: DEFAULT_NEGOTIATED_PROTOCOL_VERSION + maybeSendPrimingEvent(STANDALONE_SSE_STREAM_ID, sseSession, clientProtocolVersion) + // Keep the SSE connection open until the client disconnects or the transport is closed. + // Cleanup uses try/finally (runs during cancellation propagation) instead of + // invokeOnCompletion (runs after job completion) to minimize the window between + // disconnect and mapping removal. Identity check ensures only this stream's entry + // is removed — not a replacement that arrived in the meantime. + try { + awaitCancellation() + } finally { + // Atomic compare-and-remove — only evict our own entry, not a replacement's. + streamsMapping.remove(STANDALONE_SSE_STREAM_ID, newContext) + } + } + + /** Handles an HTTP DELETE request by closing the session and the transport. */ + public suspend fun handleDeleteRequest(call: ApplicationCall) { + if (!validateSession(call) || !validateProtocolVersion(call)) return + sessionId?.let { onSessionClosed?.invoke(it) } + close() + call.respond(HttpStatusCode.OK) + } + + /** + * Closes the SSE stream associated with the given [requestId], prompting the client to reconnect. + * Useful for implementing polling behavior for long-running operations. + */ + public suspend fun closeSseStream(requestId: RequestId) { + if (configuration.enableJsonResponse) return + val streamId = requestToStreamMapping[requestId] ?: return + val sessionContext = streamsMapping[streamId] ?: return + + withContext(NonCancellable) { + try { + sessionContext.session?.close() + } catch (e: Exception) { + _onError(e) + } finally { + streamsMapping.remove(streamId) + } + } + } + + private suspend fun replayEvents(store: EventStore, lastEventId: String, session: ServerSSESession) { + val call: ApplicationCall = session.call + + try { + var lookupSupported = true + val lookupStreamId = try { + store.getStreamIdForEventId(lastEventId) + } catch (_: NotImplementedError) { + lookupSupported = false + null + } catch (_: UnsupportedOperationException) { + lookupSupported = false + null + } + + if (lookupSupported) { + val streamId = lookupStreamId + ?: run { + call.reject( + HttpStatusCode.BadRequest, + RPCError.ErrorCode.CONNECTION_CLOSED, + "Invalid event ID format", + ) + return + } + + if (streamId in streamsMapping) { + call.reject( + HttpStatusCode.Conflict, + RPCError.ErrorCode.CONNECTION_CLOSED, + "Conflict: Stream already has an active connection", + ) + return + } + } + + // SSE headers are already set by the framework's SSE handler. + flushSse(session) + + val streamId = store.replayEventsAfter(lastEventId) { eventId, message -> + try { + session.send( + event = "message", + id = eventId, + data = McpJson.encodeToString(message), + ) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + _onError(IllegalStateException("Failed to replay event: ${e.message}", e)) + } + } + + streamsMapping[streamId] = SessionContext(session, call) + + session.coroutineContext.job.invokeOnCompletion { throwable -> + streamsMapping.remove(streamId) + throwable?.let { _onError(it) } + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + _onError(e) + } + } + + private suspend fun validateSession(call: ApplicationCall, id: RequestId? = null): Boolean { + if (sessionIdGenerator == null) return true + + if (!initialized.load()) { + call.reject( + HttpStatusCode.BadRequest, + RPCError.ErrorCode.CONNECTION_CLOSED, + "Bad Request: Server not initialized", + id, + ) + return false + } + + val headerId = call.request.header(MCP_SESSION_ID_HEADER) + + if (headerId == null) { + call.reject( + HttpStatusCode.BadRequest, + RPCError.ErrorCode.CONNECTION_CLOSED, + "Bad Request: Mcp-Session-Id header is required", + id, + ) + return false + } + + return when (headerId) { + sessionId -> true + + else -> { + call.reject( + HttpStatusCode.NotFound, + REQUEST_TIMEOUT, + "Session not found", + id, + ) + false + } + } + } + + private suspend fun validateProtocolVersion(call: ApplicationCall, id: RequestId? = null): Boolean { + val version = call.request.headers[MCP_PROTOCOL_VERSION_HEADER] ?: return true + + return when (version) { + !in SUPPORTED_PROTOCOL_VERSIONS -> { + call.reject( + HttpStatusCode.BadRequest, + RPCError.ErrorCode.CONNECTION_CLOSED, + "Bad Request: Unsupported protocol version (supported versions: ${ + SUPPORTED_PROTOCOL_VERSIONS.joinToString( + ", ", + ) + })", + id, + ) + false + } + + else -> true + } + } + + private fun validateHeaders(call: ApplicationCall): String? { + if (!configuration.enableDnsRebindingProtection) return null + + configuration.allowedHosts?.let { hosts -> + val hostHeader = call.request.headers[HttpHeaders.Host] + val hostname = hostHeader?.let { extractHostname(it) }?.lowercase() + val allowedHostsLowercase = hosts.map { + extractHostname(it)?.lowercase() ?: it.lowercase() + } + + if (hostname == null || hostname !in allowedHostsLowercase) { + return "Invalid Host header: $hostHeader" + } + } + + configuration.allowedOrigins?.let { origins -> + val originHeader = call.request.headers[HttpHeaders.Origin]?.lowercase() + val allowedOriginsLowercase = origins.map { it.lowercase() } + + // Allow requests without Origin (non-browser clients cannot perform DNS rebinding) + if (originHeader != null && originHeader !in allowedOriginsLowercase) { + return "Invalid Origin header: $originHeader" + } + } + + return null + } + + private suspend fun flushSse(session: ServerSSESession?) { + try { + session?.send(data = "") + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + _onError(e) + } + } + + private suspend fun parseBody(call: ApplicationCall): List? { + val body = try { + call.receiveTextWithLimit(configuration.maxRequestBodySize) + } catch (e: RequestBodyTooLargeException) { + call.reject( + HttpStatusCode.PayloadTooLarge, + RPCError.ErrorCode.INVALID_REQUEST, + "Invalid Request: message size exceeds maximum of ${e.maxBodySize} bytes", + ) + return null + } + + return when (val element = McpJson.parseToJsonElement(body)) { + is JsonObject -> listOf(McpJson.decodeFromJsonElement(element)) + + is JsonArray -> McpJson.decodeFromJsonElement>(element) + + else -> { + call.reject( + HttpStatusCode.BadRequest, + RPCError.ErrorCode.INVALID_REQUEST, + "Invalid Request: unable to parse JSON body", + ) + null + } + } + } + + /** + * Reports whether the `Accept` header admits [mime]. + * + * Comparison is over parsed media ranges rather than raw substrings, so wildcards are honored + * and a near miss such as `application/jsonp` no longer passes as `application/json`. An absent + * header states no preference and admits any type. + */ + private fun String?.accepts(mime: ContentType): Boolean { + if (this == null) return true + return parseHeaderValue(this).any { range -> + if (range.quality <= 0.0) return@any false + try { + mime.match(ContentType.parse(range.value)) + } catch (_: BadContentTypeFormatException) { + false + } + } + } + + private suspend fun emitOnStream(streamId: String, session: ServerSSESession?, message: JSONRPCMessage) { + val eventId = configuration.eventStore?.storeEvent(streamId, message) + try { + session?.send(event = "message", id = eventId, data = McpJson.encodeToString(message)) + } catch (e: CancellationException) { + // Atomic compare-and-remove — only evict this stream's entry, not a replacement's. + val current = streamsMapping[streamId] + if (current != null && current.session === session) { + streamsMapping.remove(streamId, current) + } + throw e + } catch (_: Exception) { + val current = streamsMapping[streamId] + if (current != null && current.session === session) { + streamsMapping.remove(streamId, current) + } + } + } + + private suspend fun maybeSendPrimingEvent( + streamId: String, + session: ServerSSESession?, + clientProtocolVersion: String, + ) { + val store = configuration.eventStore + if (store == null || session == null) return + // Priming events have empty data which older clients cannot handle. + // Only send priming events to clients with protocol version >= 2025-11-25 + // which includes the fix for handling empty SSE data. + if (clientProtocolVersion < MIN_PRIMING_EVENT_PROTOCOL_VERSION) return + try { + val primingEventId = store.storeEvent(streamId, JSONRPCEmptyMessage) + session.send( + id = primingEventId, + retry = configuration.retryInterval?.inWholeMilliseconds, + data = "", + ) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + _onError(e) + } + } + + private fun ApplicationCall.appendSseHeaders() { + this.response.headers.append(HttpHeaders.ContentType, ContentType.Text.EventStream.toString()) + this.response.headers.append(HttpHeaders.CacheControl, "no-cache, no-transform") + this.response.headers.append(HttpHeaders.Connection, "keep-alive") + sessionId?.let { this.response.headers.append(MCP_SESSION_ID_HEADER, it) } + this.response.status(HttpStatusCode.OK) + } +} + +/** Writes an already serialized body so a client accepting only `text/event-stream` still receives [status]. */ +internal suspend fun ApplicationCall.reject( + status: HttpStatusCode, + code: Int, + message: String, + id: RequestId? = null, +) { + val error = JSONRPCError(id = id, error = RPCError(code = code, message = message)) + respondText(McpJson.encodeToString(error), ContentType.Application.Json, status) +} diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/TransportManager.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/TransportManager.kt new file mode 100644 index 0000000..e02931f --- /dev/null +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/TransportManager.kt @@ -0,0 +1,22 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport +import kotlinx.atomicfu.AtomicRef +import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.update +import kotlinx.collections.immutable.PersistentMap +import kotlinx.collections.immutable.toPersistentMap + +internal class TransportManager { + private val transports: AtomicRef> = atomic(emptyMap().toPersistentMap()) + + fun getTransport(sessionId: String): T? = transports.value[sessionId] + + fun addTransport(sessionId: String, transport: T) { + transports.update { it.put(sessionId, transport) } + } + + fun removeTransport(sessionId: String) { + transports.update { it.remove(sessionId) } + } +} diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensions.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensions.kt index a3d2fd3..a05daef 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensions.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensions.kt @@ -1,12 +1,82 @@ package io.modelcontextprotocol.kotlin.sdk.server +import io.github.oshai.kotlinlogging.KotlinLogging +import io.ktor.server.application.Application +import io.ktor.server.application.install import io.ktor.server.routing.Route +import io.ktor.server.routing.routing import io.ktor.server.websocket.WebSocketServerSession +import io.ktor.server.websocket.WebSockets import io.ktor.server.websocket.webSocket -import io.modelcontextprotocol.kotlin.sdk.Implementation +import io.ktor.utils.io.CancellationException +import io.ktor.utils.io.KtorDsl import io.modelcontextprotocol.kotlin.sdk.LIB_VERSION -import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities import io.modelcontextprotocol.kotlin.sdk.shared.IMPLEMENTATION_NAME +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import kotlinx.coroutines.awaitCancellation + +private val logger = KotlinLogging.logger {} + +/** + * Configures the Ktor Application to handle Model Context Protocol (MCP) over WebSocket. + */ +@KtorDsl +public fun Route.mcpWebSocket(block: () -> Server) { + webSocket { + mcpWebSocketEndpoint(block) + } +} + +/** + * Configures the Ktor Application to handle Model Context Protocol (MCP) over WebSocket. + */ +@KtorDsl +public fun Route.mcpWebSocket(path: String, block: () -> Server) { + webSocket(path) { + mcpWebSocketEndpoint(block) + } +} + +/** + * Configures the Ktor Application to handle Model Context Protocol (MCP) over WebSocket. + */ +@KtorDsl +public fun Application.mcpWebSocket(block: () -> Server) { + install(WebSockets) + + routing { + mcpWebSocket(block) + } +} + +/** + * Configures the Ktor Application to handle Model Context Protocol (MCP) over WebSocket at the specified path. + */ +@KtorDsl +public fun Application.mcpWebSocket(path: String, block: () -> Server) { + install(WebSockets) + + routing { + mcpWebSocket(path, block) + } +} + +internal suspend fun WebSocketServerSession.mcpWebSocketEndpoint(block: () -> Server) { + logger.info { "Ktor Server establishing new connection" } + val transport = createMcpTransport(this) + val server = block() + var session: ServerSession? = null + try { + session = server.createSession(transport) + awaitCancellation() + } catch (_: CancellationException) { + session?.close() + } +} + +private fun createMcpTransport(webSocketSession: WebSocketServerSession): WebSocketMcpServerTransport = + WebSocketMcpServerTransport(webSocketSession) /** * Registers a WebSocket route that establishes an MCP (Model Context Protocol) server session. @@ -14,6 +84,11 @@ import io.modelcontextprotocol.kotlin.sdk.shared.IMPLEMENTATION_NAME * @param options Optional server configuration settings for the MCP server. * @param handler A suspend function that defines the server's behavior. */ +@Deprecated( + "Use mcpWebSocket with a lambda that returns a Server instance instead", + ReplaceWith(""), + DeprecationLevel.WARNING, +) public fun Route.mcpWebSocket(options: ServerOptions? = null, handler: suspend Server.() -> Unit = {}) { webSocket { createMcpServer(this, options, handler) @@ -27,6 +102,11 @@ public fun Route.mcpWebSocket(options: ServerOptions? = null, handler: suspend S * @param options Optional server configuration settings for the MCP server. * @param handler A suspend function that defines the server's behavior. */ +@Deprecated( + "Use mcpWebSocket with a path and a lambda that returns a Server instance instead", + ReplaceWith(""), + DeprecationLevel.WARNING, +) public fun Route.mcpWebSocket(path: String, options: ServerOptions? = null, handler: suspend Server.() -> Unit = {}) { webSocket(path) { createMcpServer(this, options, handler) @@ -38,6 +118,11 @@ public fun Route.mcpWebSocket(path: String, options: ServerOptions? = null, hand * * @param handler A suspend function that defines the behavior of the transport layer. */ +@Deprecated( + "Use mcpWebSocket with a lambda that returns a Server instance instead", + ReplaceWith(""), + DeprecationLevel.WARNING, +) public fun Route.mcpWebSocketTransport(handler: suspend WebSocketMcpServerTransport.() -> Unit = {}) { webSocket { val transport = createMcpTransport(this) @@ -53,6 +138,11 @@ public fun Route.mcpWebSocketTransport(handler: suspend WebSocketMcpServerTransp * @param path The URL path at which to register the WebSocket route. * @param handler A suspend function that defines the behavior of the transport layer. */ +@Deprecated( + "Use mcpWebSocket with a path and a lambda that returns a Server instance instead", + ReplaceWith("Routing.mcpWebSocket"), + DeprecationLevel.WARNING, +) public fun Route.mcpWebSocketTransport(path: String, handler: suspend WebSocketMcpServerTransport.() -> Unit = {}) { webSocket(path) { val transport = createMcpTransport(this) @@ -62,7 +152,7 @@ public fun Route.mcpWebSocketTransport(path: String, handler: suspend WebSocketM } } -private suspend fun Route.createMcpServer( +private suspend fun createMcpServer( session: WebSocketServerSession, options: ServerOptions?, handler: suspend Server.() -> Unit, @@ -83,10 +173,7 @@ private suspend fun Route.createMcpServer( ), ) - server.connect(transport) + server.createSession(transport) handler(server) server.close() } - -private fun createMcpTransport(session: WebSocketServerSession): WebSocketMcpServerTransport = - WebSocketMcpServerTransport(session) diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpServerTransport.kt index 877fda5..0c0cc78 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpServerTransport.kt @@ -1,10 +1,13 @@ package io.modelcontextprotocol.kotlin.sdk.server +import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.http.HttpHeaders import io.ktor.server.websocket.WebSocketServerSession import io.modelcontextprotocol.kotlin.sdk.shared.MCP_SUBPROTOCOL import io.modelcontextprotocol.kotlin.sdk.shared.WebSocketMcpTransport +private val logger = KotlinLogging.logger {} + /** * Server-side implementation of the MCP (Model Context Protocol) transport over WebSocket. * @@ -12,6 +15,7 @@ import io.modelcontextprotocol.kotlin.sdk.shared.WebSocketMcpTransport */ public class WebSocketMcpServerTransport(override val session: WebSocketServerSession) : WebSocketMcpTransport() { override suspend fun initializeSession() { + logger.debug { "Checking session headers" } val subprotocol = session.call.request.headers[HttpHeaders.SecWebSocketProtocol] if (subprotocol != MCP_SUBPROTOCOL) { error("Invalid subprotocol: $subprotocol, expected $MCP_SUBPROTOCOL") diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/utils/ToolNameValidation.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/utils/ToolNameValidation.kt new file mode 100644 index 0000000..a63c2e4 --- /dev/null +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/utils/ToolNameValidation.kt @@ -0,0 +1,60 @@ +package io.modelcontextprotocol.kotlin.sdk.server.utils + +import io.github.oshai.kotlinlogging.KotlinLogging + +private val logger = KotlinLogging.logger("ToolNameValidation") + +private const val MAX_TOOL_NAME_LENGTH = 128 + +private const val SEP_986_URL = "https://github.com/modelcontextprotocol/modelcontextprotocol/issues/986" + +/** + * Validates a tool name against the MCP tool naming standard (SEP-986). + * Returns a list of warnings. An empty list means the name is valid. + */ +internal fun validateToolName(name: String): List { + if (name.isEmpty()) return listOf("Tool name cannot be empty") + + val invalidChars = name.toSet().filterNot { it.isAsciiAllowed() } + val remainingInvalidChars = invalidChars - setOf(' ', ',') + + return listOfNotNull( + "Tool name exceeds maximum length of $MAX_TOOL_NAME_LENGTH characters (current: ${name.length})" + .takeIf { name.length > MAX_TOOL_NAME_LENGTH }, + "Tool name contains spaces, which may cause parsing issues" + .takeIf { ' ' in invalidChars }, + "Tool name contains commas, which may cause parsing issues" + .takeIf { ',' in invalidChars }, + "Tool name contains invalid characters: ${remainingInvalidChars.joinToString(", ") { "\"$it\"" }}" + .takeIf { remainingInvalidChars.isNotEmpty() }, + "Allowed characters are: A-Z, a-z, 0-9, underscore (_), dash (-), and dot (.)" + .takeIf { invalidChars.isNotEmpty() }, + "Tool name starts or ends with a dash, which may cause parsing issues in some contexts" + .takeIf { name.startsWith('-') || name.endsWith('-') }, + "Tool name starts or ends with a dot, which may cause parsing issues in some contexts" + .takeIf { name.startsWith('.') || name.endsWith('.') }, + ) +} + +/** + * Validates the tool [name] and logs a warning if it does not conform to SEP-986. + * Does not block registration — tools with non-conforming names are still accepted. + */ +internal fun warnIfInvalidToolName(name: String) { + val warnings = validateToolName(name) + if (warnings.isEmpty()) return + + val warningLines = warnings.joinToString("\n") { " - $it" } + logger.warn { + """ + |Tool name validation warning for "$name": + |$warningLines + |Tool registration will proceed, but this may cause compatibility issues. + |Consider updating the tool name to conform to the MCP tool naming standard. + |See SEP-986: $SEP_986_URL + """.trimMargin() + } +} + +private fun Char.isAsciiAllowed(): Boolean = + this in 'a'..'z' || this in 'A'..'Z' || this in '0'..'9' || this == '_' || this == '-' || this == '.' diff --git a/kotlin-sdk-server/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/FeatureRegistryTest.kt b/kotlin-sdk-server/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/FeatureRegistryTest.kt new file mode 100644 index 0000000..c6bf72b --- /dev/null +++ b/kotlin-sdk-server/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/FeatureRegistryTest.kt @@ -0,0 +1,106 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.kotest.matchers.shouldBe +import kotlin.test.Test +import kotlin.test.assertFailsWith + +private data class TestFeature(override val key: FeatureKey, val payload: String = "") : Feature + +private class RecordingListener : FeatureListener { + val updatedKeys = mutableListOf() + + override fun onFeatureUpdated(featureKey: String) { + updatedKeys.add(featureKey) + } +} + +class FeatureRegistryTest { + + private val registry = FeatureRegistry("Tool") + private val listener = RecordingListener() + + init { + registry.addListener(listener) + } + + @Test + fun `add should register feature and notify listener`() { + val feature = TestFeature("a") + + registry.add(feature) + + registry.values shouldBe mapOf("a" to feature) + listener.updatedKeys shouldBe listOf("a") + } + + @Test + fun `add should throw when key is already registered`() { + registry.add(TestFeature("a")) + + val exception = assertFailsWith { + registry.add(TestFeature("a", payload = "override")) + } + + exception.message shouldBe "Tool \"a\" is already registered. Remove it first to replace it." + } + + @Test + fun `add should keep original feature and not notify when duplicate is rejected`() { + val original = TestFeature("a", payload = "original") + registry.add(original) + + assertFailsWith { + registry.add(TestFeature("a", payload = "override")) + } + + registry.values shouldBe mapOf("a" to original) + listener.updatedKeys shouldBe listOf("a") + } + + @Test + fun `addAll should register all features and notify per feature`() { + val a = TestFeature("a") + val b = TestFeature("b") + + registry.addAll(listOf(a, b)) + + registry.values shouldBe mapOf("a" to a, "b" to b) + listener.updatedKeys shouldBe listOf("a", "b") + } + + @Test + fun `addAll should throw on duplicate keys within batch and register nothing`() { + val exception = assertFailsWith { + registry.addAll(listOf(TestFeature("a"), TestFeature("b"), TestFeature("a"))) + } + + exception.message shouldBe "Duplicate Tool keys in batch: [a]" + registry.values shouldBe emptyMap() + listener.updatedKeys shouldBe emptyList() + } + + @Test + fun `addAll should throw when any key is already registered and register nothing`() { + val existing = TestFeature("existing") + registry.add(existing) + + val exception = assertFailsWith { + registry.addAll(listOf(TestFeature("new"), TestFeature("existing", payload = "override"))) + } + + exception.message shouldBe "Tools already registered: [existing]. Remove them first to replace them." + registry.values shouldBe mapOf("existing" to existing) + listener.updatedKeys shouldBe listOf("existing") + } + + @Test + fun `add should succeed after the existing feature is removed`() { + registry.add(TestFeature("a", payload = "original")) + registry.remove("a") + + registry.add(TestFeature("a", payload = "replacement")) + + registry.values shouldBe mapOf("a" to TestFeature("a", payload = "replacement")) + listener.updatedKeys shouldBe listOf("a", "a", "a") + } +} diff --git a/kotlin-sdk-server/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSessionAssertCapabilityTest.kt b/kotlin-sdk-server/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSessionAssertCapabilityTest.kt new file mode 100644 index 0000000..a172e96 --- /dev/null +++ b/kotlin-sdk-server/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSessionAssertCapabilityTest.kt @@ -0,0 +1,147 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.kotest.matchers.string.shouldContain +import io.modelcontextprotocol.kotlin.sdk.shared.Transport +import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.RequestId +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.toJSON +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertFailsWith + +/** + * Tests for the protected helpers [ServerSession.assertCapabilityForMethod] and + * [ServerSession.assertNotificationCapability]. + * + * Both helpers are `protected`, so this suite uses a small subclass [TestServerSession] + * that re-exports them via [TestServerSession.exposedAssertCapabilityForMethod] and + * [TestServerSession.exposedAssertNotificationCapability]. Client capabilities are + * seeded by replaying an [InitializeRequest] through [InitializeReplayTransport]: + * the transport drives the inbound side of the handshake on `start()`, which causes + * the session's internal [Method.Defined.Initialize] handler to populate + * [ServerSession.clientCapabilities]. + */ +class ServerSessionAssertCapabilityTest { + + @Test + fun `server TasksGet throws when client has no tasks capability`() = runTest { + val session = newTestServerSession(clientCapabilities = ClientCapabilities()) + val ex = assertFailsWith { + session.exposedAssertCapabilityForMethod(Method.Defined.TasksGet) + } + ex.message.orEmpty() shouldContain "Client does not support tasks" + } + + @Test + fun `server TasksGet succeeds when client declared tasks`() = runTest { + val session = newTestServerSession( + clientCapabilities = ClientCapabilities(tasks = ClientCapabilities.Tasks()), + ) + session.exposedAssertCapabilityForMethod(Method.Defined.TasksGet) + } + + @Test + fun `server TasksList throws when client tasks list is null`() = runTest { + val session = newTestServerSession( + clientCapabilities = ClientCapabilities(tasks = ClientCapabilities.Tasks()), + ) + val ex = assertFailsWith { + session.exposedAssertCapabilityForMethod(Method.Defined.TasksList) + } + ex.message.orEmpty() shouldContain "Client does not support listing tasks" + } + + @Test + fun `server TasksCancel throws when client tasks cancel is null`() = runTest { + val session = newTestServerSession( + clientCapabilities = ClientCapabilities(tasks = ClientCapabilities.Tasks()), + ) + val ex = assertFailsWith { + session.exposedAssertCapabilityForMethod(Method.Defined.TasksCancel) + } + ex.message.orEmpty() shouldContain "Client does not support cancelling tasks" + } + + @Test + fun `server NotificationsTasksStatus throws when server has no tasks capability`() = runTest { + val session = newTestServerSession(serverCapabilities = ServerCapabilities()) + val ex = assertFailsWith { + session.exposedAssertNotificationCapability(Method.Defined.NotificationsTasksStatus) + } + ex.message.orEmpty() shouldContain "Server does not support tasks" + } + + private suspend fun newTestServerSession( + clientCapabilities: ClientCapabilities = ClientCapabilities(), + serverCapabilities: ServerCapabilities = ServerCapabilities(), + ): TestServerSession { + val session = TestServerSession( + serverInfo = Implementation("test-server", "1.0.0"), + options = ServerOptions(capabilities = serverCapabilities), + instructions = null, + ) + val transport = InitializeReplayTransport(clientCapabilities) + session.connect(transport) + return session + } + + /** + * Test-only [ServerSession] subclass that exposes the protected + * [ServerSession.assertCapabilityForMethod] and + * [ServerSession.assertNotificationCapability] helpers to the test code. + */ + private class TestServerSession(serverInfo: Implementation, options: ServerOptions, instructions: String?) : + ServerSession(serverInfo, options, instructions) { + fun exposedAssertCapabilityForMethod(method: Method): Unit = assertCapabilityForMethod(method) + fun exposedAssertNotificationCapability(method: Method): Unit = assertNotificationCapability(method) + } + + /** + * Minimal in-memory [Transport] that drives the server-side initialize handshake. + * + * On [start], it dispatches an [InitializeRequest] carrying the configured + * [clientCapabilities] to the session via [onMessage]. The session's internal + * `initialize` handler runs synchronously and populates + * [ServerSession.clientCapabilities] before [start] returns. Other JSON-RPC + * traffic the session subsequently emits is silently consumed. + */ + private class InitializeReplayTransport(private val clientCapabilities: ClientCapabilities) : Transport { + private var onMessageBlock: (suspend (JSONRPCMessage) -> Unit)? = null + private var onCloseBlock: (() -> Unit)? = null + + override suspend fun start() { + val initializeRequest = InitializeRequest( + params = InitializeRequestParams( + protocolVersion = LATEST_PROTOCOL_VERSION, + capabilities = clientCapabilities, + clientInfo = Implementation("mock-client", "1.0.0"), + ), + ).toJSON().copy(id = RequestId.NumberId(1)) + onMessageBlock?.invoke(initializeRequest) + } + + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) = Unit + + override suspend fun close() { + onCloseBlock?.invoke() + } + + override fun onMessage(block: suspend (JSONRPCMessage) -> Unit) { + onMessageBlock = block + } + + override fun onClose(block: () -> Unit) { + onCloseBlock = block + } + + override fun onError(block: (Throwable) -> Unit) = Unit + } +} diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/AbstractKtorExtensionsTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/AbstractKtorExtensionsTest.kt new file mode 100644 index 0000000..7639bc6 --- /dev/null +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/AbstractKtorExtensionsTest.kt @@ -0,0 +1,67 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.kotest.assertions.ktor.client.shouldHaveContentType +import io.kotest.assertions.ktor.client.shouldHaveStatus +import io.ktor.client.HttpClient +import io.ktor.client.request.post +import io.ktor.client.request.prepareGet +import io.ktor.client.request.setBody +import io.ktor.client.statement.bodyAsChannel +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType +import io.ktor.utils.io.readUTF8Line +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities + +abstract class AbstractKtorExtensionsTest { + + protected val sseContentType = ContentType("text", "event-stream") + + protected fun testServer() = Server( + serverInfo = Implementation(name = "test-server", version = "1.0.0"), + options = ServerOptions(capabilities = ServerCapabilities()), + ) + + /** + * Asserts that both MCP transport endpoints are registered at [path]: + * - GET returns 200 with `text/event-stream` content type (SSE endpoint) + * - POST with a valid MCP payload and session returns 202 Accepted + * - POST without a sessionId returns 400 Bad Request + */ + protected suspend fun HttpClient.assertMcpEndpointsAt(path: String) { + prepareGet(path).execute { response -> + response.shouldHaveStatus(HttpStatusCode.OK) + response.shouldHaveContentType(sseContentType) + + // Extract sessionId from the SSE "endpoint" event + val channel = response.bodyAsChannel() + var eventName: String? = null + var sessionId: String? = null + + while (sessionId == null && !channel.isClosedForRead) { + val line = channel.readUTF8Line() ?: break + when { + line.startsWith("event:") -> eventName = line.substringAfter("event:").trim() + + line.startsWith("data:") && eventName == "endpoint" -> { + val data = line.substringAfter("data:").trim() + sessionId = data.substringAfter("sessionId=").ifEmpty { null } + } + } + } + + requireNotNull(sessionId) { "sessionId not found in SSE endpoint event" } + + // POST a valid JSON-RPC ping while the SSE connection is alive + val postResponse = post("$path?sessionId=$sessionId") { + contentType(ContentType.Application.Json) + setBody("""{"jsonrpc":"2.0","id":1,"method":"ping"}""") + } + postResponse.shouldHaveStatus(HttpStatusCode.Accepted) + } + + // POST without sessionId returns 400 Bad Request + post(path).shouldHaveStatus(HttpStatusCode.BadRequest) + } +} diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/DnsRebindingProtectionTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/DnsRebindingProtectionTest.kt new file mode 100644 index 0000000..ea38219 --- /dev/null +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/DnsRebindingProtectionTest.kt @@ -0,0 +1,428 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.kotest.assertions.ktor.client.shouldHaveStatus +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldNotContain +import io.ktor.client.request.header +import io.ktor.client.request.post +import io.ktor.client.statement.bodyAsText +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType +import io.ktor.server.application.install +import io.ktor.server.response.respondText +import io.ktor.server.routing.post +import io.ktor.server.routing.route +import io.ktor.server.routing.routing +import io.ktor.server.sse.SSE +import io.ktor.server.testing.ApplicationTestBuilder +import io.ktor.server.testing.testApplication +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.junit.jupiter.params.provider.ValueSource +import kotlin.test.Test + +class DnsRebindingProtectionTest { + + companion object { + @JvmStatic + fun invalidHostCases(): List = listOf( + Arguments.of("", "Invalid Host header: (malformed or missing)"), + Arguments.of("evil.com", "Invalid Host: evil.com"), + ) + + @JvmStatic + fun extractHostnameAcceptCases(): List = listOf( + Arguments.of("localhost", "localhost"), + Arguments.of("localhost:3000", "localhost"), + Arguments.of("127.0.0.1:8080", "127.0.0.1"), + Arguments.of("[::1]", "[::1]"), + Arguments.of("[::1]:3000", "[::1]"), + Arguments.of("localhost:", "localhost"), + Arguments.of("[::1]:", "[::1]"), + ) + + @JvmStatic + fun extractHostnameRejectCases(): List = listOf( + "", // empty + "evil.com@localhost", // userinfo + "evil.com/path", // path + "evil.com?q=1", // query + "evil.com#frag", // fragment + "[::1", // malformed IPv6 + "[]", // empty brackets + "[]:3000", // empty brackets + port + " localhost", // leading whitespace + "localhost ", // trailing whitespace + "localhost\t:80", // embedded tab + "localhost:abc", // non-numeric port + "localhost:-1", // negative port + "[::1]:abc", // non-numeric IPv6 port + ":80", // leading colon + ) + + @JvmStatic + fun extractOriginHostAcceptCases(): List = listOf( + Arguments.of("http://example.com", "example.com"), + Arguments.of("http://example.com:8080", "example.com"), + Arguments.of("https://Example.COM", "Example.COM"), + Arguments.of("https://example.com", "example.com"), + ) + + @JvmStatic + fun extractOriginHostRejectCases(): List = listOf( + "example.com", // no scheme + "not-a-url", // unparseable + "", // empty + ) + } + + private fun testWithPlugin( + config: DnsRebindingProtectionConfig.() -> Unit = {}, + test: suspend ApplicationTestBuilder.() -> Unit, + ): Unit = testApplication { + application { + install(SSE) + routing { + route("/mcp") { + install(DnsRebindingProtection, config) + post { call.respondText("ok") } + } + } + } + test() + } + + @ParameterizedTest + @ValueSource(strings = ["localhost", "127.0.0.1", "[::1]", "localhost:3000", "[::1]:8080"]) + fun `plugin accepts valid Host header`(hostHeader: String) = testWithPlugin { + val response = client.post("/mcp") { + header(HttpHeaders.Host, hostHeader) + } + response.shouldHaveStatus(HttpStatusCode.OK) + response.bodyAsText() shouldBe "ok" + } + + @ParameterizedTest + @MethodSource("invalidHostCases") + fun `plugin rejects invalid Host header`(hostHeader: String, expectedBody: String) = testWithPlugin { + val response = client.post("/mcp") { + header(HttpHeaders.Host, hostHeader) + } + response.shouldHaveStatus(HttpStatusCode.Forbidden) + response.bodyAsText() shouldContain expectedBody + } + + @Test + fun `plugin does not echo raw malformed Host in rejection`() = testWithPlugin { + val response = client.post("/mcp") { + header(HttpHeaders.Host, "evil.com@localhost") + } + response.shouldHaveStatus(HttpStatusCode.Forbidden) + response.bodyAsText() shouldNotContain "evil.com@localhost" + response.bodyAsText() shouldContain "malformed or missing" + } + + @Test + fun `plugin allows request without Origin header`() = testWithPlugin( + config = { allowedOrigins = listOf("http://localhost:3000") }, + ) { + val response = client.post("/mcp") { + header(HttpHeaders.Host, "localhost") + } + response.shouldHaveStatus(HttpStatusCode.OK) + response.bodyAsText() shouldBe "ok" + } + + @Test + fun `plugin rejects disallowed Origin`() = testWithPlugin( + config = { allowedOrigins = listOf("http://localhost:3000") }, + ) { + val response = client.post("/mcp") { + header(HttpHeaders.Host, "localhost") + header(HttpHeaders.Origin, "http://evil.com") + } + response.shouldHaveStatus(HttpStatusCode.Forbidden) + response.bodyAsText() shouldContain "Invalid Origin host: evil.com" + } + + @Test + fun `plugin allows matching Origin`() = testWithPlugin( + config = { allowedOrigins = listOf("http://localhost:3000") }, + ) { + val response = client.post("/mcp") { + header(HttpHeaders.Host, "localhost") + header(HttpHeaders.Origin, "http://localhost:3000") + } + response.shouldHaveStatus(HttpStatusCode.OK) + response.bodyAsText() shouldBe "ok" + } + + @Test + fun `plugin allows Origin with different port but same hostname`() = testWithPlugin( + config = { allowedOrigins = listOf("http://localhost:3000") }, + ) { + val response = client.post("/mcp") { + header(HttpHeaders.Host, "localhost") + header(HttpHeaders.Origin, "http://localhost:9999") + } + response.shouldHaveStatus(HttpStatusCode.OK) + response.bodyAsText() shouldBe "ok" + } + + @Test + fun `plugin allows Origin with different scheme but same hostname`() = testWithPlugin( + config = { allowedOrigins = listOf("http://localhost:3000") }, + ) { + val response = client.post("/mcp") { + header(HttpHeaders.Host, "localhost") + header(HttpHeaders.Origin, "https://localhost:3000") + } + response.shouldHaveStatus(HttpStatusCode.OK) + response.bodyAsText() shouldBe "ok" + } + + @Test + fun `plugin rejects unparseable Origin header`() = testWithPlugin( + config = { allowedOrigins = listOf("http://localhost:3000") }, + ) { + val response = client.post("/mcp") { + header(HttpHeaders.Host, "localhost") + header(HttpHeaders.Origin, "not-a-url") + } + response.shouldHaveStatus(HttpStatusCode.Forbidden) + response.bodyAsText() shouldContain "Invalid Origin header: (unparseable)" + } + + @Test + fun `plugin with custom allowedHosts accepts matching host`() = testWithPlugin( + config = { allowedHosts = listOf("myapp.com") }, + ) { + val response = client.post("/mcp") { + header(HttpHeaders.Host, "myapp.com:443") + } + response.shouldHaveStatus(HttpStatusCode.OK) + response.bodyAsText() shouldBe "ok" + } + + @Test + fun `plugin with custom allowedHosts rejects non-matching host`() = testWithPlugin( + config = { allowedHosts = listOf("myapp.com") }, + ) { + val response = client.post("/mcp") { + header(HttpHeaders.Host, "localhost") + } + response.shouldHaveStatus(HttpStatusCode.Forbidden) + } + + @Test + fun `host validation is case insensitive`() = testWithPlugin( + config = { allowedHosts = listOf("MyApp.COM") }, + ) { + val response = client.post("/mcp") { + header(HttpHeaders.Host, "myapp.com") + } + response.shouldHaveStatus(HttpStatusCode.OK) + response.bodyAsText() shouldBe "ok" + } + + @Test + fun `origin validation is case insensitive`() = testWithPlugin( + config = { + allowedHosts = listOf("localhost") + allowedOrigins = listOf("https://MyApp.COM") + }, + ) { + val response = client.post("/mcp") { + header(HttpHeaders.Host, "localhost") + header(HttpHeaders.Origin, "https://myapp.com") + } + response.shouldHaveStatus(HttpStatusCode.OK) + response.bodyAsText() shouldBe "ok" + } + + @Test + fun `plugin with empty allowedHosts rejects all requests`() = testWithPlugin( + config = { allowedHosts = emptyList() }, + ) { + val localhostResponse = client.post("/mcp") { + header(HttpHeaders.Host, "localhost") + } + localhostResponse.shouldHaveStatus(HttpStatusCode.Forbidden) + localhostResponse.bodyAsText() shouldContain "Invalid Host: localhost" + + val otherResponse = client.post("/mcp") { + header(HttpHeaders.Host, "myapp.com") + } + otherResponse.shouldHaveStatus(HttpStatusCode.Forbidden) + otherResponse.bodyAsText() shouldContain "Invalid Host: myapp.com" + } + + @Test + fun `plugin fails to install when allowedHosts contains an invalid host`() { + val ex = shouldThrow { + testWithPlugin( + config = { allowedHosts = listOf("https://example.com") }, + ) { + client.post("/mcp") { header(HttpHeaders.Host, "example.com") } + } + } + ex.message shouldContain "https://example.com" + } + + @Test + fun `plugin fails to install when allowedOrigins contains an invalid origin`() { + val ex = shouldThrow { + testWithPlugin( + config = { allowedOrigins = listOf("not-a-url") }, + ) { + client.post("/mcp") { header(HttpHeaders.Host, "localhost") } + } + } + ex.message shouldContain "not-a-url" + } + + @Test + fun `Route mcp with DNS protection enabled rejects non-localhost`() = testApplication { + application { + install(SSE) + routing { + mcp { testServer() } + } + } + + val response = client.post("/") { + header(HttpHeaders.Host, "evil.com") + contentType(ContentType.Application.Json) + } + response.shouldHaveStatus(HttpStatusCode.Forbidden) + } + + @Test + fun `Route mcp with DNS protection disabled allows any host`() = testApplication { + application { + install(SSE) + routing { + mcp(enableDnsRebindingProtection = false) { testServer() } + } + } + + val response = client.post("/") { + header(HttpHeaders.Host, "evil.com") + contentType(ContentType.Application.Json) + } + // Not 403 — the request reaches the handler (may get 400 for missing sessionId, etc.) + response.shouldHaveStatus(HttpStatusCode.BadRequest) + } + + @Test + fun `mcpStreamableHttp rejects non-localhost Host by default`() = testApplication { + application { + mcpStreamableHttp { testServer() } + } + + val response = client.post("/mcp") { + header(HttpHeaders.Host, "evil.com") + contentType(ContentType.Application.Json) + } + response.shouldHaveStatus(HttpStatusCode.Forbidden) + } + + @Test + fun `mcpStatelessStreamableHttp rejects non-localhost Host by default`() = testApplication { + application { + mcpStatelessStreamableHttp { testServer() } + } + + val response = client.post("/mcp") { + header(HttpHeaders.Host, "evil.com") + contentType(ContentType.Application.Json) + } + response.shouldHaveStatus(HttpStatusCode.Forbidden) + } + + // -- default Origin validation (secure-by-default for localhost) -- + + @Test + fun `mcpStreamableHttp rejects hostile Origin by default`() = testApplication { + application { + mcpStreamableHttp { testServer() } + } + + val response = client.post("/mcp") { + header(HttpHeaders.Host, "localhost") + header(HttpHeaders.Origin, "http://evil.com") + contentType(ContentType.Application.Json) + } + response.shouldHaveStatus(HttpStatusCode.Forbidden) + response.bodyAsText() shouldContain "Invalid Origin host: evil.com" + } + + @Test + fun `mcpStreamableHttp allows localhost Origin by default`() = testApplication { + application { + mcpStreamableHttp { testServer() } + } + + val response = client.post("/mcp") { + header(HttpHeaders.Host, "localhost") + header(HttpHeaders.Origin, "http://localhost:5173") + contentType(ContentType.Application.Json) + } + response.status shouldNotBe HttpStatusCode.Forbidden + } + + @Test + fun `mcpStreamableHttp with custom allowedHosts does not auto-validate Origin`() = testApplication { + application { + mcpStreamableHttp(allowedHosts = listOf("myapp.com")) { testServer() } + } + + val response = client.post("/mcp") { + header(HttpHeaders.Host, "myapp.com") + header(HttpHeaders.Origin, "http://evil.com") + contentType(ContentType.Application.Json) + } + response.status shouldNotBe HttpStatusCode.Forbidden + } + + // -- extractHostname unit tests -- + + @ParameterizedTest + @MethodSource("extractHostnameAcceptCases") + fun `extractHostname accepts valid host`(input: String, expected: String) { + extractHostname(input) shouldBe expected + } + + @ParameterizedTest + @MethodSource("extractHostnameRejectCases") + fun `extractHostname rejects invalid host`(input: String) { + extractHostname(input) shouldBe null + } + + // -- extractOriginHost unit tests -- + + @ParameterizedTest + @MethodSource("extractOriginHostAcceptCases") + fun `extractOriginHost extracts hostname`(input: String, expected: String) { + extractOriginHost(input) shouldBe expected + } + + @ParameterizedTest + @MethodSource("extractOriginHostRejectCases") + fun `extractOriginHost returns null for invalid origin`(input: String) { + extractOriginHost(input) shouldBe null + } + + private fun testServer(): Server = Server( + serverInfo = Implementation(name = "test-server", version = "1.0.0"), + options = ServerOptions(capabilities = ServerCapabilities()), + ) +} diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ElicitationValidationTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ElicitationValidationTest.kt new file mode 100644 index 0000000..b218b47 --- /dev/null +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ElicitationValidationTest.kt @@ -0,0 +1,438 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.modelcontextprotocol.kotlin.sdk.types.BooleanSchema +import io.modelcontextprotocol.kotlin.sdk.types.DoubleSchema +import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.EnumOption +import io.modelcontextprotocol.kotlin.sdk.types.IntegerSchema +import io.modelcontextprotocol.kotlin.sdk.types.LegacyTitledEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.McpJson +import io.modelcontextprotocol.kotlin.sdk.types.PrimitiveSchemaDefinition +import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import io.modelcontextprotocol.kotlin.sdk.types.StringSchema +import io.modelcontextprotocol.kotlin.sdk.types.StringSchemaFormat +import io.modelcontextprotocol.kotlin.sdk.types.TitledMultiSelectEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.TitledSingleSelectEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.UntitledMultiSelectEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.UntitledSingleSelectEnumSchema +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.add +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonArray +import kotlinx.serialization.json.putJsonObject +import org.junit.jupiter.api.assertDoesNotThrow +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +/** + * Unit tests for [validateElicitationContent]: pure-function checks of accepted form-mode + * content against the requested schema. + * + * End-to-end tests for the `createElicitation` round trip live in the `integration-test` + * module under `io.modelcontextprotocol.kotlin.sdk.client`. + */ +class ElicitationValidationTest { + + private fun schema(vararg properties: Pair, required: List? = null) = + ElicitRequestParams.RequestedSchema(properties = mapOf(*properties), required = required) + + private fun assertRejected( + schema: ElicitRequestParams.RequestedSchema, + content: JsonObject, + expectedError: String, + ) { + val exception = assertFailsWith { validateElicitationContent(schema, content) } + assertEquals(RPCError.ErrorCode.INVALID_PARAMS, exception.code) + val message = exception.message.orEmpty() + assertTrue( + message.startsWith("Elicitation response content does not match requested schema: "), + "Unexpected message prefix: $message", + ) + assertTrue(expectedError in message, "Expected '$expectedError' in: $message") + } + + @Test + fun `validate content matching schema passes`() { + assertDoesNotThrow { + validateElicitationContent( + schema( + "name" to StringSchema(minLength = 1, maxLength = 10), + "age" to IntegerSchema(minimum = 0, maximum = 120), + "score" to DoubleSchema(minimum = 0.0, maximum = 1.0), + "subscribed" to BooleanSchema(), + "color" to UntitledSingleSelectEnumSchema(enumValues = listOf("red", "green")), + "size" to TitledSingleSelectEnumSchema( + oneOf = listOf(EnumOption("s", "Small"), EnumOption("l", "Large")), + ), + "tags" to UntitledMultiSelectEnumSchema( + items = UntitledMultiSelectEnumSchema.Items(enumValues = listOf("a", "b", "c")), + minItems = 1, + maxItems = 2, + ), + required = listOf("name", "age"), + ), + buildJsonObject { + put("name", "octocat") + put("age", 42) + put("score", 0.5) + put("subscribed", true) + put("color", "red") + put("size", "l") + putJsonArray("tags") { add("a") } + }, + ) + } + } + + @Test + fun `validate missing required property fails`() { + assertRejected( + schema("name" to StringSchema(), required = listOf("name")), + JsonObject(emptyMap()), + "must have required property 'name'", + ) + } + + @Test + fun `validate empty content with no required properties passes`() { + assertDoesNotThrow { + validateElicitationContent(schema("name" to StringSchema()), JsonObject(emptyMap())) + } + } + + @Test + fun `validate wrong type for string property fails`() { + assertRejected( + schema("name" to StringSchema()), + buildJsonObject { put("name", 42) }, + "'name' must be string", + ) + } + + @Test + fun `validate string length constraints fail`() { + assertRejected( + schema("name" to StringSchema(minLength = 3)), + buildJsonObject { put("name", "ab") }, + "'name' must NOT have fewer than 3 characters", + ) + assertRejected( + schema("name" to StringSchema(maxLength = 3)), + buildJsonObject { put("name", "abcd") }, + "'name' must NOT have more than 3 characters", + ) + } + + @Test + fun `validate json string is not accepted for integer boolean or number`() { + // JsonPrimitive("42") is the JSON string "42", not the number 42; the validator must not + // parse string content as a number. + assertRejected( + schema("age" to IntegerSchema()), + buildJsonObject { put("age", "42") }, + "'age' must be integer", + ) + assertRejected( + schema("score" to DoubleSchema()), + buildJsonObject { put("score", "1.5") }, + "'score' must be number", + ) + assertRejected( + schema("subscribed" to BooleanSchema()), + buildJsonObject { put("subscribed", "true") }, + "'subscribed' must be boolean", + ) + } + + @Test + fun `validate integer outside minimum and maximum fails`() { + assertRejected( + schema("age" to IntegerSchema(minimum = 0)), + buildJsonObject { put("age", -1) }, + "'age' must be >= 0", + ) + assertRejected( + schema("age" to IntegerSchema(maximum = 120)), + buildJsonObject { put("age", 200) }, + "'age' must be <= 120", + ) + } + + @Test + fun `validate non-integral value for integer property fails`() { + assertRejected( + schema("age" to IntegerSchema()), + buildJsonObject { put("age", 3.5) }, + "'age' must be integer", + ) + } + + @Test + fun `validate integral number forms are accepted for integer property`() { + // JSON Schema's "integer" matches numbers with a zero fractional part. + assertDoesNotThrow { + validateElicitationContent( + schema("age" to IntegerSchema(minimum = 0, maximum = 200)), + buildJsonObject { put("age", 5.0) }, + ) + } + } + + @Test + fun `validate exponent form is accepted for integer property`() { + val content = McpJson.parseToJsonElement("""{"age": 1e2}""") as JsonObject + assertDoesNotThrow { + validateElicitationContent( + schema("age" to IntegerSchema(minimum = 0, maximum = 200)), + content, + ) + } + } + + @Test + fun `validate number outside range fails`() { + assertRejected( + schema("score" to DoubleSchema(maximum = 1.0)), + buildJsonObject { put("score", 1.5) }, + "'score' must be <= 1.0", + ) + } + + @Test + fun `validate integral value is accepted for number property`() { + assertDoesNotThrow { + validateElicitationContent( + schema("score" to DoubleSchema(minimum = 0.0)), + buildJsonObject { put("score", 5) }, + ) + } + } + + @Test + fun `validate string length counts unicode code points`() { + // JSON Schema string length counts RFC 8259 characters: one emoji is one character + // even when it is two UTF-16 units. + assertDoesNotThrow { + validateElicitationContent( + schema("emoji" to StringSchema(maxLength = 1)), + buildJsonObject { put("emoji", "😀") }, + ) + } + assertRejected( + schema("emoji" to StringSchema(minLength = 2)), + buildJsonObject { put("emoji", "😀") }, + "'emoji' must NOT have fewer than 2 characters", + ) + } + + @Test + fun `validate string format is treated as an annotation`() { + assertDoesNotThrow { + validateElicitationContent( + schema("email" to StringSchema(format = StringSchemaFormat.Email)), + buildJsonObject { put("email", "not-an-email") }, + ) + } + } + + @Test + fun `validate negative zero satisfies an inclusive zero bound`() { + // IEEE 754 comparison: -0.0 == 0.0, so an inclusive minimum of 0.0 admits -0.0. + assertDoesNotThrow { + validateElicitationContent( + schema("score" to DoubleSchema(minimum = 0.0)), + buildJsonObject { put("score", -0.0) }, + ) + } + } + + @Test + fun `validate enum value not in allowed set fails`() { + assertRejected( + schema("color" to UntitledSingleSelectEnumSchema(enumValues = listOf("red", "green"))), + buildJsonObject { put("color", "blue") }, + "'color' must be equal to one of the allowed values", + ) + assertRejected( + schema("size" to TitledSingleSelectEnumSchema(oneOf = listOf(EnumOption("s", "Small")))), + buildJsonObject { put("size", "xl") }, + "'size' must be equal to one of the allowed values", + ) + assertRejected( + schema("legacy" to LegacyTitledEnumSchema(enumValues = listOf("a", "b"))), + buildJsonObject { put("legacy", "c") }, + "'legacy' must be equal to one of the allowed values", + ) + } + + @Test + fun `validate multi-select constraints fail`() { + val tags = UntitledMultiSelectEnumSchema( + items = UntitledMultiSelectEnumSchema.Items(enumValues = listOf("a", "b", "c")), + minItems = 1, + maxItems = 2, + ) + assertRejected( + schema("tags" to tags), + buildJsonObject { put("tags", "a") }, + "'tags' must be array", + ) + assertRejected( + schema("tags" to tags), + buildJsonObject { + putJsonArray("tags") { + add("a") + add(42) + } + }, + "'tags' items must be string", + ) + assertRejected( + schema("tags" to tags), + buildJsonObject { + putJsonArray("tags") { + add("a") + add("z") + } + }, + "'tags' items must be equal to one of the allowed values", + ) + assertRejected( + schema("tags" to tags), + buildJsonObject { putJsonArray("tags") {} }, + "'tags' must NOT have fewer than 1 items", + ) + assertRejected( + schema("tags" to tags), + buildJsonObject { + putJsonArray("tags") { + add("a") + add("b") + add("c") + } + }, + "'tags' must NOT have more than 2 items", + ) + } + + @Test + fun `validate titled multi-select membership uses option consts`() { + val sizes = TitledMultiSelectEnumSchema( + items = TitledMultiSelectEnumSchema.Items( + anyOf = listOf(EnumOption("s", "Small"), EnumOption("l", "Large")), + ), + ) + assertDoesNotThrow { + validateElicitationContent( + schema("sizes" to sizes), + buildJsonObject { putJsonArray("sizes") { add("s") } }, + ) + } + assertRejected( + schema("sizes" to sizes), + buildJsonObject { putJsonArray("sizes") { add("Small") } }, + "'sizes' items must be equal to one of the allowed values", + ) + } + + @Test + fun `validate json null is rejected with the type error for each schema kind`() { + // JsonNull is a JsonPrimitive whose content is "null"; it must fail the type check, not + // accidentally parse as a value. + assertRejected( + schema("name" to StringSchema()), + buildJsonObject { put("name", JsonNull) }, + "'name' must be string", + ) + assertRejected( + schema("age" to IntegerSchema()), + buildJsonObject { put("age", JsonNull) }, + "'age' must be integer", + ) + assertRejected( + schema("subscribed" to BooleanSchema()), + buildJsonObject { put("subscribed", JsonNull) }, + "'subscribed' must be boolean", + ) + } + + @Test + fun `validate non-primitive value is rejected for scalar schemas`() { + assertRejected( + schema("name" to StringSchema()), + buildJsonObject { putJsonArray("name") { add("x") } }, + "'name' must be string", + ) + assertRejected( + schema("age" to IntegerSchema()), + buildJsonObject { putJsonObject("age") {} }, + "'age' must be integer", + ) + } + + @Test + fun `validate number below minimum fails`() { + assertRejected( + schema("score" to DoubleSchema(minimum = 0.0)), + buildJsonObject { put("score", -0.5) }, + "'score' must be >= 0.0", + ) + } + + @Test + fun `validate non-finite value for number property fails`() { + // McpJson is lenient, so an unquoted NaN literal can arrive off the wire; it decodes to + // the same primitive as Double.NaN here. + assertRejected( + schema("score" to DoubleSchema(minimum = 0.0, maximum = 1.0)), + buildJsonObject { put("score", Double.NaN) }, + "'score' must be number", + ) + } + + @Test + fun `validate required property not declared in properties is still required`() { + // JSON Schema's `required` is independent of `properties`. + assertRejected( + schema("name" to StringSchema(), required = listOf("name", "consent")), + buildJsonObject { put("name", "octocat") }, + "must have required property 'consent'", + ) + } + + @Test + fun `validate properties not declared in schema pass`() { + // JSON Schema default: without an additionalProperties restriction, undeclared keys are permitted. + assertDoesNotThrow { + validateElicitationContent( + schema("name" to StringSchema(), required = listOf("name")), + buildJsonObject { + put("name", "octocat") + put("undeclared", 42) + }, + ) + } + } + + @Test + fun `validate reports every violation in one error`() { + val exception = assertFailsWith { + validateElicitationContent( + schema( + "name" to StringSchema(), + "age" to IntegerSchema(), + required = listOf("name"), + ), + buildJsonObject { put("age", "old") }, + ) + } + val message = exception.message.orEmpty() + assertTrue("must have required property 'name'" in message, message) + assertTrue("'age' must be integer" in message, message) + } +} diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorApplicationExtensionsTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorApplicationExtensionsTest.kt new file mode 100644 index 0000000..9888867 --- /dev/null +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorApplicationExtensionsTest.kt @@ -0,0 +1,42 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.kotest.assertions.ktor.client.shouldHaveStatus +import io.kotest.matchers.shouldBe +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText +import io.ktor.http.HttpStatusCode +import io.ktor.server.response.respondText +import io.ktor.server.routing.get +import io.ktor.server.routing.routing +import io.ktor.server.testing.testApplication +import kotlin.test.Test + +/** + * Integration tests for Ktor Application.mcp() extension. + * + * Verifies that Application.mcp() installs SSE automatically and registers + * MCP endpoints at the application root, without requiring explicit install(SSE). + */ +class KtorApplicationExtensionsTest : AbstractKtorExtensionsTest() { + + /** + * Verifies that Application.mcp() does not interfere with other routes + * added to the same application. + */ + @Test + fun `Application mcp should install SSE and coexist with other routes`() = testApplication { + application { + mcp(enableDnsRebindingProtection = false) { testServer() } + + routing { + get("/health") { call.respondText("healthy") } + } + } + + val healthResponse = client.get("/health") + healthResponse.shouldHaveStatus(HttpStatusCode.OK) + healthResponse.bodyAsText() shouldBe "healthy" + + client.assertMcpEndpointsAt("/") + } +} diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorRouteExtensionsTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorRouteExtensionsTest.kt new file mode 100644 index 0000000..d6b95b4 --- /dev/null +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorRouteExtensionsTest.kt @@ -0,0 +1,125 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.kotest.assertions.ktor.client.shouldHaveStatus +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import io.ktor.client.request.get +import io.ktor.client.request.post +import io.ktor.client.statement.bodyAsText +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.install +import io.ktor.server.response.respondText +import io.ktor.server.routing.get +import io.ktor.server.routing.route +import io.ktor.server.routing.routing +import io.ktor.server.sse.SSE +import io.ktor.server.testing.testApplication +import kotlin.test.Test +import kotlin.test.assertFailsWith + +/** + * Integration tests for Ktor Route.mcp() extensions. + * + * Verifies issue #237: Route.mcp() should work on subpaths and with middleware. + * The key issue was that Routing.mcp() registered at top-level, preventing use on subpaths. + * Now Route.mcp() allows registration on any route path. + */ +class KtorRouteExtensionsTest : AbstractKtorExtensionsTest() { + + /** + * Verifies that Route.mcp() throws immediately at route registration time + * when the SSE plugin has not been installed, rather than failing silently on the first request. + */ + @Test + fun `Route mcp should throw at registration time if SSE plugin is not installed`() { + val exception = assertFailsWith { + testApplication { + application { + // Intentionally omit install(SSE) + routing { + mcp { testServer() } + } + } + client.get("/") + } + } + exception.message shouldContain "SSE" + exception.message shouldContain "install" + } + + /** + * Verifies that Route.mcp() registers SSE and POST endpoints on the subpath, + * not at root, and does not disturb sibling routes. + */ + @Test + fun `Route mcp should register SSE and POST endpoints at the given subpath`() = testApplication { + application { + install(SSE) + + routing { + get("/") { call.respondText("root") } + + route("/api/mcp") { + get("/test") { call.respondText("test-endpoint") } + mcp(enableDnsRebindingProtection = false) { testServer() } + } + } + } + + // Sibling routes are unaffected + val rootResponse = client.get("/") + rootResponse.shouldHaveStatus(HttpStatusCode.OK) + rootResponse.bodyAsText() shouldBe "root" + + val testResponse = client.get("/api/mcp/test") + testResponse.shouldHaveStatus(HttpStatusCode.OK) + testResponse.bodyAsText() shouldBe "test-endpoint" + + client.assertMcpEndpointsAt("/api/mcp") + } + + /** + * Verifies that Route.mcp() registers endpoints at the correct fully-qualified + * path when nested inside multiple route() calls. + */ + @Test + fun `Route mcp should register endpoints at the full nested path`() = testApplication { + application { + install(SSE) + + routing { + route("/v1") { + route("/services") { + route("/mcp") { + mcp(enableDnsRebindingProtection = false) { testServer() } + } + } + } + } + } + + client.assertMcpEndpointsAt("/v1/services/mcp") + } + + /** + * Verifies that mcp(path) registers SSE and POST endpoints at the sub-path + * relative to the enclosing route, not at the root. + */ + @Test + fun `Route mcp with path parameter should register endpoints at the resolved path`() = testApplication { + application { + install(SSE) + + routing { + route("/api") { + mcp("/mcp-endpoint", enableDnsRebindingProtection = false) { testServer() } + } + } + } + + client.assertMcpEndpointsAt("/api/mcp-endpoint") + + // Root /api is not an MCP endpoint + client.post("/api").shouldHaveStatus(HttpStatusCode.NotFound) + } +} diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/LogCapture.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/LogCapture.kt new file mode 100644 index 0000000..dcf3ad2 --- /dev/null +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/LogCapture.kt @@ -0,0 +1,35 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.read.ListAppender +import org.slf4j.LoggerFactory + +/** + * Captures log events from the given logger (and its children) using a Logback [ListAppender]. + * + * Usage: + * ``` + * LogCapture("io.modelcontextprotocol.kotlin.sdk.server").use { logs -> + * // … code that emits log messages … + * logs.messages.shouldExist { "expected text" in it } + * } + * ``` + */ +internal class LogCapture(loggerName: String) : AutoCloseable { + private val logger: Logger = LoggerFactory.getLogger(loggerName) as Logger + private val appender: ListAppender = ListAppender() + + init { + appender.start() + logger.addAppender(appender) + } + + val messages: List + get() = appender.list.map { it.formattedMessage } + + override fun close() { + logger.detachAppender(appender) + appender.stop() + } +} diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/RequestBodyTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/RequestBodyTest.kt new file mode 100644 index 0000000..9dc30cf --- /dev/null +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/RequestBodyTest.kt @@ -0,0 +1,103 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.kotest.matchers.shouldBe +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.client.statement.bodyAsText +import io.ktor.http.HttpStatusCode +import io.ktor.server.response.respondText +import io.ktor.server.routing.post +import io.ktor.server.routing.routing +import io.ktor.server.testing.ApplicationTestBuilder +import io.ktor.server.testing.testApplication +import kotlin.test.Test + +class RequestBodyTest { + + /** Wires a POST endpoint that echoes the body when it fits, or replies 413 when it is too large. */ + private fun ApplicationTestBuilder.installEchoEndpoint(maxBytes: Long) { + install(io.ktor.server.sse.SSE) + application { + routing { + post("/echo") { + val text = try { + call.receiveTextWithLimit(maxBytes) + } catch (e: RequestBodyTooLargeException) { + call.respondText(e.message ?: "too large", status = HttpStatusCode.PayloadTooLarge) + return@post + } + call.respondText(text) + } + } + } + } + + @Test + fun `body under the limit is returned intact`() = testApplication { + installEchoEndpoint(maxBytes = 1024) + val payload = "hello world" + + val response = client.post("/echo") { setBody(payload) } + + response.status shouldBe HttpStatusCode.OK + response.bodyAsText() shouldBe payload + } + + @Test + fun `body exactly at the limit is accepted`() = testApplication { + val payload = "x".repeat(64) + installEchoEndpoint(maxBytes = payload.length.toLong()) + + val response = client.post("/echo") { setBody(payload) } + + response.status shouldBe HttpStatusCode.OK + response.bodyAsText() shouldBe payload + } + + @Test + fun `body exceeding the limit is rejected with 413`() = testApplication { + val payload = "x".repeat(65) + installEchoEndpoint(maxBytes = 64) + + val response = client.post("/echo") { setBody(payload) } + + response.status shouldBe HttpStatusCode.PayloadTooLarge + } + + @Test + fun `large body exceeding the limit is rejected without buffering it whole`() = testApplication { + // 8 MB body against a 4 MB limit: must be rejected. + installEchoEndpoint(maxBytes = 4L * 1024 * 1024) + val payload = "x".repeat(8 * 1024 * 1024) + + val response = client.post("/echo") { setBody(payload) } + + response.status shouldBe HttpStatusCode.PayloadTooLarge + } + + @Test + fun `multibyte UTF-8 body larger than a read chunk decodes intact`() = testApplication { + // Emoji is 4 bytes in UTF-8; a payload larger than the internal read chunk forces the + // accumulated bytes to span multiple chunks. Decoding once at the end must not corrupt + // characters that straddle a chunk boundary. + val unit = "a😀b" // mixes 1- and 4-byte code units so boundaries land mid-character + val payload = unit.repeat(40_000) // well over a 64 KiB read chunk + installEchoEndpoint(maxBytes = payload.encodeToByteArray().size.toLong()) + + val response = client.post("/echo") { setBody(payload) } + + response.status shouldBe HttpStatusCode.OK + response.bodyAsText() shouldBe payload + } + + @Test + fun `body is returned intact when the limit is Long MAX_VALUE`() = testApplication { + installEchoEndpoint(maxBytes = Long.MAX_VALUE) + val payload = "hello world" + + val response = client.post("/echo") { setBody(payload) } + + response.status shouldBe HttpStatusCode.OK + response.bodyAsText() shouldBe payload + } +} diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingTest.kt new file mode 100644 index 0000000..cfcbb62 --- /dev/null +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingTest.kt @@ -0,0 +1,104 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.modelcontextprotocol.kotlin.sdk.types.Role +import io.modelcontextprotocol.kotlin.sdk.types.SamplingMessage +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.ToolResultContent +import io.modelcontextprotocol.kotlin.sdk.types.ToolUseContent +import kotlinx.serialization.json.JsonObject +import org.junit.jupiter.api.assertDoesNotThrow +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +/** + * Unit tests for [validateSamplingMessages]: pure-function checks of the SEP-1577 + * last-two-messages tool_use / tool_result rules. + * + * End-to-end and capability-enforcement tests for `Server.createMessage` live in the + * `integration-test` module under the same package. + */ +class SamplingTest { + + private fun toolUse(id: String) = ToolUseContent(id = id, name = "t", input = JsonObject(emptyMap())) + private fun toolResult(id: String) = ToolResultContent(toolUseId = id, content = emptyList()) + + @Test + fun `validate empty message list is valid`() { + assertDoesNotThrow { validateSamplingMessages(emptyList()) } + } + + @Test + fun `validate text-only conversation is valid`() { + assertDoesNotThrow { + validateSamplingMessages( + listOf( + SamplingMessage(Role.User, TextContent("hi")), + SamplingMessage(Role.Assistant, TextContent("hello")), + ), + ) + } + } + + @Test + fun `validate matched tool_use and tool_result at boundary is valid`() { + assertDoesNotThrow { + validateSamplingMessages( + listOf( + SamplingMessage(Role.Assistant, listOf(TextContent("using tool"), toolUse("c1"))), + SamplingMessage(Role.User, toolResult("c1")), + ), + ) + } + } + + @Test + fun `validate orphan tool_result with no previous message fails`() { + assertFailsWith { + validateSamplingMessages( + listOf(SamplingMessage(Role.User, toolResult("missing"))), + ) + } + } + + @Test + fun `validate tool_result mixed with text in last message fails`() { + assertFailsWith { + validateSamplingMessages( + listOf( + SamplingMessage(Role.Assistant, toolUse("c1")), + SamplingMessage(Role.User, listOf(toolResult("c1"), TextContent("extra"))), + ), + ) + } + } + + @Test + fun `validate tool_result ids must match tool_use ids in previous message`() { + assertFailsWith { + validateSamplingMessages( + listOf( + SamplingMessage(Role.Assistant, toolUse("c1")), + SamplingMessage(Role.User, toolResult("wrong_id")), + ), + ) + } + } + + @Test + fun `validate tool_use requires explicit tool_result in last message`() { + val error = assertFailsWith { + validateSamplingMessages( + listOf( + SamplingMessage(Role.Assistant, toolUse("c1")), + SamplingMessage(Role.User, TextContent("missing result")), + ), + ) + } + + assertEquals( + "tool_use blocks from previous message must be followed by matching tool_result blocks", + error.message, + ) + } +} diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SseServerTransportTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SseServerTransportTest.kt new file mode 100644 index 0000000..a0756f3 --- /dev/null +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SseServerTransportTest.kt @@ -0,0 +1,90 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.kotest.matchers.shouldBe +import io.ktor.client.request.post +import io.ktor.client.request.prepareGet +import io.ktor.client.request.setBody +import io.ktor.client.statement.HttpResponse +import io.ktor.client.statement.bodyAsChannel +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType +import io.ktor.server.application.install +import io.ktor.server.routing.post +import io.ktor.server.routing.routing +import io.ktor.server.sse.SSE +import io.ktor.server.sse.ServerSSESession +import io.ktor.server.testing.testApplication +import io.ktor.utils.io.readLine +import io.mockk.mockk +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.test.Test + +class SseServerTransportTest : AbstractKtorExtensionsTest() { + + @Test + fun `handlePostMessage on a not-started transport does not deliver the message`() = testApplication { + // A registered onMessage callback opens the delivery gate, so if the not-initialized branch + // falls through it would wrongly hand the message to the application. It must return instead. + val transport = SseServerTransport("/messages", mockk(relaxed = true)) + val delivered = AtomicBoolean(false) + transport.onMessage { delivered.set(true) } + + application { + routing { + post("/messages") { transport.handlePostMessage(call) } + } + } + + val response = client.post("/messages") { + contentType(ContentType.Application.Json) + setBody("""{"jsonrpc":"2.0","id":1,"method":"ping"}""") + } + + response.status shouldBe HttpStatusCode.InternalServerError + delivered.get() shouldBe false + } + + @Test + fun `SSE POST exceeding maxRequestBodySize is rejected with 413`() = testApplication { + application { + install(SSE) + routing { + mcp(enableDnsRebindingProtection = false, maxRequestBodySize = MAX_BODY) { testServer() } + } + } + + client.prepareGet("/").execute { response -> + val sessionId = response.readSessionId() + requireNotNull(sessionId) { "sessionId not found in SSE endpoint event" } + + // A body one byte over the configured limit must be rejected before processing. + val oversized = "x".repeat((MAX_BODY + 1).toInt()) + val postResponse = client.post("/?sessionId=$sessionId") { + contentType(ContentType.Application.Json) + setBody(oversized) + } + postResponse.status shouldBe HttpStatusCode.PayloadTooLarge + } + } + + private suspend fun HttpResponse.readSessionId(): String? { + val channel = bodyAsChannel() + var eventName: String? = null + while (!channel.isClosedForRead) { + val line = channel.readLine() ?: break + when { + line.startsWith("event:") -> eventName = line.substringAfter("event:").trim() + + line.startsWith("data:") && eventName == "endpoint" -> { + return line.substringAfter("data:").trim().substringAfter("sessionId=").ifEmpty { null } + } + } + } + return null + } + + private companion object { + const val MAX_BODY = 1024L + } +} diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StatelessStreamableHttpSessionLifecycleTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StatelessStreamableHttpSessionLifecycleTest.kt new file mode 100644 index 0000000..3646faf --- /dev/null +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StatelessStreamableHttpSessionLifecycleTest.kt @@ -0,0 +1,100 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.kotest.assertions.nondeterministic.eventually +import io.kotest.matchers.maps.shouldBeEmpty +import io.kotest.matchers.shouldBe +import io.ktor.client.request.HttpRequestBuilder +import io.ktor.client.request.header +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType +import io.ktor.server.testing.testApplication +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION +import io.modelcontextprotocol.kotlin.sdk.types.McpJson +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.toJSON +import kotlin.test.Test +import kotlin.time.Duration.Companion.seconds + +/** + * Verifies that the stateless Streamable HTTP endpoint does not leak server sessions: + * every session created to serve a single request must be closed and removed from the + * session registry once that request completes (https://github.com/modelcontextprotocol/kotlin-sdk/issues/786). + */ +class StatelessStreamableHttpSessionLifecycleTest { + + private fun testServer(): Server = Server( + Implementation("test-server", "1.0"), + ServerOptions(capabilities = ServerCapabilities()), + ) + + @Test + fun `stateless endpoint removes session after each request`() = testApplication { + val server = testServer() + application { + mcpStatelessStreamableHttp { server } + } + + repeat(5) { + val response = client.post("/mcp") { + addStreamableHeaders() + setBody(initializeRequestBody()) + } + response.status shouldBe HttpStatusCode.OK + } + + eventually(5.seconds) { + server.sessions.shouldBeEmpty() + } + } + + @Test + fun `stateless endpoint removes session when the request is rejected`() = testApplication { + val server = testServer() + application { + mcpStatelessStreamableHttp { server } + } + + // Missing "text/event-stream" in Accept makes the transport reject the request. + val response = client.post("/mcp") { + header(HttpHeaders.Host, "localhost") + header(HttpHeaders.Accept, ContentType.Application.Json.toString()) + contentType(ContentType.Application.Json) + setBody(initializeRequestBody()) + } + response.status shouldBe HttpStatusCode.NotAcceptable + + eventually(5.seconds) { + server.sessions.shouldBeEmpty() + } + } + + private fun HttpRequestBuilder.addStreamableHeaders() { + header(HttpHeaders.Host, "localhost") + header( + HttpHeaders.Accept, + listOf(ContentType.Application.Json, ContentType.Text.EventStream).joinToString(", "), + ) + contentType(ContentType.Application.Json) + } + + private fun initializeRequestBody(): String { + val request = InitializeRequest( + InitializeRequestParams( + protocolVersion = LATEST_PROTOCOL_VERSION, + capabilities = ClientCapabilities(), + clientInfo = Implementation(name = "test-client", version = "1.0.0"), + ), + ).toJSON() + + return McpJson.encodeToString(JSONRPCMessage.serializer(), request) + } +} diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt index be1e64e..c6370a1 100644 --- a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt @@ -1,28 +1,59 @@ package io.modelcontextprotocol.kotlin.sdk.server -import io.modelcontextprotocol.kotlin.sdk.InitializedNotification -import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage -import io.modelcontextprotocol.kotlin.sdk.PingRequest +import io.kotest.assertions.nondeterministic.eventually +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.assertions.withClue +import io.kotest.matchers.collections.shouldContain +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain import io.modelcontextprotocol.kotlin.sdk.shared.ReadBuffer import io.modelcontextprotocol.kotlin.sdk.shared.serializeMessage -import io.modelcontextprotocol.kotlin.sdk.toJSON +import io.modelcontextprotocol.kotlin.sdk.types.InitializedNotification +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.PingRequest +import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import io.modelcontextprotocol.kotlin.sdk.types.toJSON +import io.modelcontextprotocol.kotlin.test.utils.runIntegrationTest +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.cancel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.withTimeoutOrNull +import kotlinx.io.Buffer +import kotlinx.io.RawSink +import kotlinx.io.RawSource import kotlinx.io.Sink import kotlinx.io.Source import kotlinx.io.asSink import kotlinx.io.asSource import kotlinx.io.buffered +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource import java.io.ByteArrayOutputStream +import java.io.IOException import java.io.PipedInputStream import java.io.PipedOutputStream -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertEquals +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors import kotlin.test.assertFalse import kotlin.test.assertTrue +import kotlin.test.fail +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class StdioServerTransportTest { private lateinit var input: PipedInputStream private lateinit var inputWriter: PipedOutputStream @@ -33,7 +64,7 @@ class StdioServerTransportTest { private lateinit var bufferedInput: Source private lateinit var printOutput: Sink - @BeforeTest + @BeforeEach fun setUp() { // Simulate an input stream that we can push data into using inputWriter. input = PipedInputStream() @@ -50,34 +81,37 @@ class StdioServerTransportTest { } bufferedInput = input.asSource().buffered() - printOutput = output.asSink().buffered() } @Test - fun `should start then close cleanly`() { - runBlocking { - val server = StdioServerTransport(bufferedInput, printOutput) - server.onError { error -> - throw error - } - - var didClose = false - server.onClose { - didClose = true - } - - server.start() - assertFalse(didClose, "Should not have closed yet") - - server.close() - assertTrue(didClose, "Should have closed after calling close()") - } + fun `should be safe to close before start`() = runIntegrationTest { + val server = StdioServerTransport(input = bufferedInput, output = printOutput) + server.close() // initialized guard makes this a no-op; must not throw } @Test - fun `should not read until started`() = runTest { - val server = StdioServerTransport(bufferedInput, printOutput) + fun `should start then close cleanly`() = runIntegrationTest { + val server = StdioServerTransport(input = bufferedInput, output = printOutput) + server.onError { error -> + throw error + } + + var didClose = false + server.onClose { + didClose = true + } + + server.start() + assertFalse(didClose, "Should not have closed yet") + + server.close() + assertTrue(didClose, "Should have closed after calling close()") + } + + @Test + fun `should not read until started`() = runIntegrationTest { + val server = StdioServerTransport(input = bufferedInput, output = printOutput) server.onError { error -> throw error } @@ -101,12 +135,12 @@ class StdioServerTransportTest { server.start() val received = readMessage.await() - assertEquals(message, received) + received shouldBe message } @Test - fun `should read multiple messages`() = runTest { - val server = StdioServerTransport(bufferedInput, printOutput) + fun `should read multiple messages`() = runIntegrationTest { + val server = StdioServerTransport(input = bufferedInput, output = printOutput) server.onError { error -> throw error } @@ -135,10 +169,495 @@ class StdioServerTransportTest { server.start() finished.await() - assertEquals(messages, readMessages) + readMessages shouldBe messages + } + + // region: Exception handling + + @ParameterizedTest(name = "[{index}] input throws {0}") + @MethodSource("inputErrors") + fun `should invoke onError when input stream throws`(throwable: Throwable): Unit = runIntegrationTest { + val server = StdioServerTransport(input = FaultyRawSource(throwable).buffered(), output = printOutput) + val capturedError = CompletableDeferred() + server.onError { capturedError.complete(it) } + server.onMessage {} + + server.start() + + capturedError.await() shouldBe throwable + server.close() + } + + @ParameterizedTest(name = "[{index}] output throws {0}") + @MethodSource("outputErrors") + fun `should invoke onError when output sink throws`(throwable: Throwable): Unit = runIntegrationTest { + val server = StdioServerTransport(input = bufferedInput, output = FaultyRawSink(throwable).buffered()) + val capturedError = CompletableDeferred() + server.onError { capturedError.complete(it) } + server.onMessage {} + + server.start() + server.send(PingRequest().toJSON()) + + capturedError.await() shouldBe throwable + server.close() + } + + @Test + fun `should call onClose when input EOF is reached`(): Unit = runIntegrationTest { + val server = StdioServerTransport(input = bufferedInput, output = printOutput) + val didClose = CompletableDeferred() + server.onError { throw it } + server.onClose { didClose.complete(Unit) } + server.onMessage {} + + server.start() + inputWriter.close() // signal EOF to the reading loop + + eventually(2.seconds) { + didClose.isCompleted shouldBe true + } + } + + @Test + fun `should throw when starting twice`(): Unit = runIntegrationTest { + val server = StdioServerTransport(input = bufferedInput, output = printOutput) + server.onMessage {} + server.start() + withClue("Server should not start twice") { + shouldThrow { + server.start() + } + } + server.close() + } + + @Test + fun `should report 'closed' error message when starting after close`(): Unit = runIntegrationTest { + val server = StdioServerTransport(input = bufferedInput, output = printOutput) + server.onError {} + server.onMessage {} + server.start() + server.close() + + val ex = shouldThrow { + server.start() + } + ex.message shouldContain "closed" + } + + @ParameterizedTest(name = "[{index}] handler throws {0}") + @MethodSource("handlerErrors") + fun `should continue processing messages after handler throws`(throwable: Throwable) = runIntegrationTest { + val server = StdioServerTransport(input = bufferedInput, output = printOutput) + val capturedErrors = mutableListOf() + val receivedMessages = mutableListOf() + val secondMessageProcessed = CompletableDeferred() + + val message1 = PingRequest().toJSON() + val message2 = InitializedNotification().toJSON() + + server.onError { capturedErrors.add(it) } + server.onMessage { message -> + if (message == message1) { + throw throwable + } else { + receivedMessages.add(message) + secondMessageProcessed.complete(Unit) + } + } + + server.start() + + inputWriter.write(serializeMessage(message1)) + inputWriter.write(serializeMessage(message2)) + inputWriter.flush() + + secondMessageProcessed.await() + + capturedErrors shouldContain throwable + receivedMessages shouldBe listOf(message2) + server.close() + } + + @Test + fun `should not invoke onError for CancellationException in handler`() = runIntegrationTest { + val server = StdioServerTransport(input = bufferedInput, output = printOutput) + val capturedError = CompletableDeferred() + server.onError { capturedError.complete(it) } + + server.onMessage { throw CancellationException("cancelled") } + server.start() + + inputWriter.write(serializeMessage(PingRequest().toJSON())) + inputWriter.flush() + + // We expect onError NOT to be called. + // We wait a bit to make sure it's not called, then close. + try { + withTimeout(1.seconds) { + capturedError.await() + } + fail("Should not have captured an error for CancellationException") + } catch (_: TimeoutCancellationException) { + // Success - timeout reached without error captured + } finally { + server.close() + } + } + + // endregion + + @Test + fun `should continue receiving valid messages after malformed JSON is skipped`() = runIntegrationTest { + val server = StdioServerTransport(input = bufferedInput, output = printOutput) + val received = CompletableDeferred() + // ReadBuffer silently skips unparseable lines — no onError callback expected + server.onError {} + server.onMessage { received.complete(it) } + + val validMessage = PingRequest().toJSON() + inputWriter.write("not-valid-json\n".toByteArray()) + inputWriter.write(" \t \r \n".toByteArray()) // blank line + inputWriter.write(serializeMessage(validMessage).toByteArray()) + inputWriter.flush() + + server.start() + + received.await() shouldBe validMessage + server.close() + } + + @Test + fun `should throw McpException when send is called before start`() = runIntegrationTest { + val server = StdioServerTransport(input = bufferedInput, output = printOutput) + val ex = shouldThrow { + server.send(PingRequest().toJSON()) + } + ex.code shouldBe RPCError.ErrorCode.CONNECTION_CLOSED + } + + @Test + fun `should throw McpException when send is called after close`() = runIntegrationTest { + val server = StdioServerTransport(input = bufferedInput, output = printOutput) + server.onError {} + server.onMessage {} + server.start() + server.close() + + val ex = shouldThrow { + server.send(PingRequest().toJSON()) + } + ex.code shouldBe RPCError.ErrorCode.CONNECTION_CLOSED + } + + @Test + fun `should drain in-flight outbound messages on graceful close`() = runIntegrationTest { + val server = StdioServerTransport(input = bufferedInput, output = printOutput) + server.onError {} + server.onMessage {} + server.start() + + val numMessages = 50 + repeat(numMessages) { + server.send(PingRequest().toJSON()) + } + server.close() + + val outputLines = String(output.toByteArray()).lines().count { it.isNotBlank() } + outputLines shouldBe numMessages + } + + @Test + fun `should fail-fast when reader throws non-EOF IOException`() = runIntegrationTest { + val ioError = IOException("transient stream failure") + val server = StdioServerTransport(input = FaultyRawSource(ioError).buffered(), output = printOutput) + val errorCaptured = CompletableDeferred() + val closeCaptured = CompletableDeferred() + server.onError { errorCaptured.complete(it) } + server.onClose { closeCaptured.complete(Unit) } + server.onMessage {} + + server.start() + + errorCaptured.await() shouldBe ioError + closeCaptured.await() + } + + @Test + fun `should close input source on natural EOF`() = runIntegrationTest { + val inputClosed = CompletableDeferred() + val eofSource = object : RawSource { + override fun readAtMostTo(sink: Buffer, byteCount: Long): Long = -1L + override fun close() { + inputClosed.complete(Unit) + } + } + val server = StdioServerTransport(input = eofSource.buffered(), output = printOutput) + val onCloseFired = CompletableDeferred() + server.onError {} + server.onMessage {} + server.onClose { onCloseFired.complete(Unit) } + + server.start() + + inputClosed.await() + onCloseFired.await() + } + + // region: concurrency + + @Test + fun `should be safe under concurrent start and close`() = runIntegrationTest(timeout = 30.seconds) { + val iterations = 2000 + repeat(iterations) { iteration -> + val server = StdioServerTransport(input = Buffer(), output = Buffer()) + server.onError {} + server.onMessage {} + + coroutineScope { + launch(Dispatchers.Default) { + try { + server.start() + } catch (_: IllegalStateException) { + } + } + launch(Dispatchers.Default) { + server.close() + } + } + + withClue("iteration $iteration: send() after race should fail with CONNECTION_CLOSED") { + val ex = shouldThrow { + server.send(PingRequest().toJSON()) + } + ex.code shouldBe RPCError.ErrorCode.CONNECTION_CLOSED + } + } + } + + @Test + fun `should suspend send under back-pressure`() = runIntegrationTest(timeout = 30.seconds) { + val unblock = CountDownLatch(1) + val blockingSink = object : RawSink { + override fun write(source: Buffer, byteCount: Long) { + source.clear() + unblock.await() + } + + override fun flush() { + // noop + } + + override fun close() { + unblock.countDown() + } + } + val server = StdioServerTransport(input = bufferedInput, output = blockingSink.buffered()) + server.onError {} + server.onMessage {} + server.start() + + val maxAttempts = 256 + var suspendedAt = -1 + for (i in 0 until maxAttempts) { + val ok = withTimeoutOrNull(100.milliseconds) { server.send(PingRequest().toJSON()) } + if (ok == null) { + suspendedAt = i + break + } + } + assertTrue( + suspendedAt in 0 until maxAttempts, + "send() should have suspended within $maxAttempts attempts, got suspendedAt=$suspendedAt", + ) + // Unblock writer so close() can drain. + unblock.countDown() + server.close() + } + + @Test + fun `should not hang close when handler suspends indefinitely`() = runIntegrationTest { + val server = StdioServerTransport(input = bufferedInput, output = printOutput) + val handlerEntered = CompletableDeferred() + val neverComplete = CompletableDeferred() + server.onError {} + server.onMessage { + handlerEntered.complete(Unit) + neverComplete.await() + } + server.start() + + inputWriter.write(serializeMessage(PingRequest().toJSON())) + inputWriter.flush() + + handlerEntered.await() + server.close() + } + + @Test + fun `should not hang close when onMessage was never registered`() = runIntegrationTest { + val server = StdioServerTransport(input = bufferedInput, output = printOutput) + server.onError {} + server.start() + + inputWriter.write(serializeMessage(PingRequest().toJSON())) + inputWriter.flush() + + delay(200.milliseconds) + + server.close() + } + + // endregion + + // region: scope and dispatcher knobs + + @Test + fun `should honor externally provided CoroutineScope`() = runIntegrationTest { + val externalScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + val server = StdioServerTransport(input = bufferedInput, output = printOutput) { + scope = externalScope + } + val closeCaptured = CompletableDeferred() + server.onError {} + server.onClose { closeCaptured.complete(Unit) } + server.onMessage {} + + server.start() + externalScope.cancel() + + closeCaptured.await() + + val ex = shouldThrow { + server.send(PingRequest().toJSON()) + } + ex.code shouldBe RPCError.ErrorCode.CONNECTION_CLOSED + } + + @Test + fun `should use ioDispatcher for reader and writer`() = runIntegrationTest { + val threadName = "stdio-test-io-thread" + val executor = Executors.newFixedThreadPool(2) { r -> Thread(r, threadName) } + try { + val readerThread = CompletableDeferred() + val writerThread = CompletableDeferred() + val unblockReader = CountDownLatch(1) + val recordingInput = object : RawSource { + override fun readAtMostTo(sink: Buffer, byteCount: Long): Long { + if (!readerThread.isCompleted) { + readerThread.complete(Thread.currentThread().name) + } + unblockReader.await() + return -1L + } + + override fun close() { + unblockReader.countDown() + } + } + val recordingOutput = object : RawSink { + override fun write(source: Buffer, byteCount: Long) { + if (!writerThread.isCompleted) { + writerThread.complete(Thread.currentThread().name) + } + source.clear() + } + + override fun flush() { + // noop + } + + override fun close() { + // noop + } + } + val server = StdioServerTransport( + input = recordingInput.buffered(), + output = recordingOutput.buffered(), + ) { + ioDispatcher = executor.asCoroutineDispatcher() + } + server.onError {} + server.onMessage {} + server.start() + + readerThread.await() shouldContain threadName + server.send(PingRequest().toJSON()) + writerThread.await() shouldContain threadName + + server.close() + } finally { + executor.shutdownNow() + } + } + + @Test + fun `should use handlerDispatcher for message handling`() = runIntegrationTest { + val threadName = "stdio-test-handler-thread" + val executor = Executors.newSingleThreadExecutor { r -> Thread(r, threadName) } + try { + val server = StdioServerTransport(input = bufferedInput, output = printOutput) { + handlerDispatcher = executor.asCoroutineDispatcher() + } + val observedThreadName = CompletableDeferred() + server.onError {} + server.onMessage { + observedThreadName.complete(Thread.currentThread().name) + } + server.start() + + inputWriter.write(serializeMessage(PingRequest().toJSON())) + inputWriter.flush() + + observedThreadName.await() shouldContain threadName + server.close() + } finally { + executor.shutdownNow() + } + } + + // endregion + + private fun inputErrors() = listOf( + IOException("simulated read failure"), + RuntimeException("unexpected read exception"), + OutOfMemoryError("unexpected read error"), + ) + + private fun outputErrors() = listOf( + IOException("simulated write failure"), + RuntimeException("unexpected write exception"), + OutOfMemoryError("unexpected write error"), + ) + + private fun handlerErrors() = listOf( + RuntimeException("handler failure"), + IOException("handler IO failure"), + OutOfMemoryError("handler error"), + ) + + /** A [RawSource] that immediately throws [throwable] on every read attempt. */ + private class FaultyRawSource(private val throwable: Throwable) : RawSource { + override fun readAtMostTo(sink: Buffer, byteCount: Long): Long = throw throwable + override fun close() { + // noop + } + } + + /** A [RawSink] that throws [throwable] on every [write] call. */ + private class FaultyRawSink(private val throwable: Throwable) : RawSink { + override fun write(source: Buffer, byteCount: Long): Unit = throw throwable + override fun flush() { + // noop + } + + override fun close() { + // noop + } } } -fun PipedOutputStream.write(s: String) { +private fun PipedOutputStream.write(s: String) { write(s.toByteArray()) } diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpResponseDeliveryTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpResponseDeliveryTest.kt new file mode 100644 index 0000000..fc076eb --- /dev/null +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpResponseDeliveryTest.kt @@ -0,0 +1,99 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.kotest.matchers.shouldBe +import io.ktor.client.request.delete +import io.ktor.client.request.get +import io.ktor.client.request.header +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.client.statement.HttpResponse +import io.ktor.client.statement.bodyAsText +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpMethod +import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType +import io.ktor.server.testing.testApplication +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCError +import io.modelcontextprotocol.kotlin.sdk.types.McpJson +import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import kotlin.test.Test + +/** + * A client opening the standalone stream sends `Accept: text/event-stream`, which matches no JSON + * converter. Responses rendered through ContentNegotiation used to reach such a client as an empty + * 406 with the intended status discarded. + */ +class StreamableHttpResponseDeliveryTest { + + private val eventStream = ContentType.Text.EventStream.toString() + + private fun testServer(): Server = Server( + Implementation("test-server", "1.0"), + ServerOptions(capabilities = ServerCapabilities()), + ) + + @Test + fun `stateless GET is rejected with 405 and advertises POST`() = testApplication { + application { mcpStatelessStreamableHttp { testServer() } } + + val response = client.get("/mcp") { + header(HttpHeaders.Host, "localhost") + header(HttpHeaders.Accept, eventStream) + } + + response.assertMethodNotAllowed() + } + + @Test + fun `stateless DELETE is rejected with 405 and advertises POST`() = testApplication { + application { mcpStatelessStreamableHttp { testServer() } } + + val response = client.delete("/mcp") { + header(HttpHeaders.Host, "localhost") + header(HttpHeaders.Accept, eventStream) + } + + response.assertMethodNotAllowed() + } + + @Test + fun `stateful DELETE for an unknown session returns 404`() = testApplication { + application { mcpStreamableHttp { testServer() } } + + val response = client.delete("/mcp") { + header(HttpHeaders.Host, "localhost") + header(HttpHeaders.Accept, eventStream) + header(MCP_SESSION_ID_HEADER, "unknown-session") + } + + response.status shouldBe HttpStatusCode.NotFound + response.decodeError().error.message shouldBe "Session not found" + } + + @Test + fun `POST carrying no request is acknowledged with a bodiless 202`() = testApplication { + application { mcpStatelessStreamableHttp { testServer() } } + + val response = client.post("/mcp") { + header(HttpHeaders.Host, "localhost") + header(HttpHeaders.Accept, "${ContentType.Application.Json}, $eventStream") + contentType(ContentType.Application.Json) + setBody("""{"jsonrpc":"2.0","method":"notifications/initialized"}""") + } + + response.status shouldBe HttpStatusCode.Accepted + response.bodyAsText() shouldBe "" + } + + private suspend fun HttpResponse.assertMethodNotAllowed() { + status shouldBe HttpStatusCode.MethodNotAllowed + headers[HttpHeaders.Allow] shouldBe HttpMethod.Post.value + contentType()?.withoutParameters() shouldBe ContentType.Application.Json + decodeError().error.code shouldBe RPCError.ErrorCode.CONNECTION_CLOSED + } + + private suspend fun HttpResponse.decodeError(): JSONRPCError = McpJson.decodeFromString(bodyAsText()) +} diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerConfigurationTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerConfigurationTest.kt new file mode 100644 index 0000000..1efe2ab --- /dev/null +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerConfigurationTest.kt @@ -0,0 +1,103 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.kotest.matchers.collections.shouldExist +import io.kotest.matchers.collections.shouldHaveSize +import io.ktor.client.request.get +import io.ktor.serialization.kotlinx.json.json +import io.ktor.server.application.install +import io.ktor.server.testing.testApplication +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import kotlin.test.Test +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation + +private const val MCP_SERVER_PACKAGE = "io.modelcontextprotocol.kotlin.sdk.server" + +class StreamableHttpServerConfigurationTest { + + private fun testServer(): Server = Server( + Implementation("test-server", "1.0"), + ServerOptions(capabilities = ServerCapabilities()), + ) + + @Test + fun `mcpStreamableHttp warns when ContentNegotiation is pre-installed`() { + LogCapture(MCP_SERVER_PACKAGE).use { logs -> + testApplication { + application { + install(ServerContentNegotiation) { + json() // Pre-install with default (wrong) config + } + mcpStreamableHttp { testServer() } + } + client.get("/mcp") + } + logs.messages.shouldExist { "ContentNegotiation is already installed" in it } + } + } + + @Test + fun `mcpStatelessStreamableHttp warns when ContentNegotiation is pre-installed`() { + LogCapture(MCP_SERVER_PACKAGE).use { logs -> + testApplication { + application { + install(ServerContentNegotiation) { + json() // Pre-install with default (wrong) config + } + mcpStatelessStreamableHttp { testServer() } + } + client.get("/mcp") + } + logs.messages.shouldExist { "ContentNegotiation is already installed" in it } + } + } + + @Test + fun `mcp warns when ContentNegotiation is pre-installed`() { + LogCapture(MCP_SERVER_PACKAGE).use { logs -> + testApplication { + application { + install(ServerContentNegotiation) { + json() // Pre-install with default (wrong) config + } + mcp { testServer() } + } + client.get("/sse") + } + logs.messages.shouldExist { "ContentNegotiation is already installed" in it } + } + } + + @Test + fun `installMcpContentNegotiation is idempotent`() { + LogCapture(MCP_SERVER_PACKAGE).use { logs -> + testApplication { + application { + installMcpContentNegotiation() + installMcpContentNegotiation() + } + client.get("/") + } + logs.messages.filter { "ContentNegotiation is already installed" in it } + .shouldHaveSize(0) + } + } + + @Test + fun `installMcpContentNegotiation warns exactly once when ContentNegotiation is pre-installed`() { + LogCapture(MCP_SERVER_PACKAGE).use { logs -> + testApplication { + application { + install(ServerContentNegotiation) { + json() + } + installMcpContentNegotiation() + installMcpContentNegotiation() + } + client.get("/") + } + logs.messages.filter { "ContentNegotiation is already installed" in it } + .shouldHaveSize(1) + } + } +} diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt new file mode 100644 index 0000000..43ff605 --- /dev/null +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt @@ -0,0 +1,1226 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.kotest.matchers.collections.shouldContainAll +import io.kotest.matchers.equals.shouldBeEqual +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logging +import io.ktor.client.request.HttpRequestBuilder +import io.ktor.client.request.get +import io.ktor.client.request.header +import io.ktor.client.request.post +import io.ktor.client.request.prepareGet +import io.ktor.client.request.setBody +import io.ktor.client.statement.bodyAsChannel +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType +import io.ktor.serialization.kotlinx.json.json +import io.ktor.server.application.ApplicationCall +import io.ktor.server.application.install +import io.ktor.server.routing.get +import io.ktor.server.routing.post +import io.ktor.server.routing.routing +import io.ktor.server.sse.ServerSSESession +import io.ktor.server.testing.ApplicationTestBuilder +import io.ktor.server.testing.testApplication +import io.ktor.sse.ServerSentEvent +import io.ktor.utils.io.ByteReadChannel +import io.ktor.utils.io.readLine +import io.ktor.utils.io.readUTF8Line +import io.modelcontextprotocol.kotlin.sdk.types.CancelledNotification +import io.modelcontextprotocol.kotlin.sdk.types.CancelledNotificationParams +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.InitializedNotification +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCError +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse +import io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION +import io.modelcontextprotocol.kotlin.sdk.types.ListResourcesResult +import io.modelcontextprotocol.kotlin.sdk.types.ListToolsResult +import io.modelcontextprotocol.kotlin.sdk.types.McpJson +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import io.modelcontextprotocol.kotlin.sdk.types.RequestId +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.Tool +import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema +import io.modelcontextprotocol.kotlin.sdk.types.toJSON +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withTimeout +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.coroutines.CoroutineContext +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation as ClientContentNegotiation +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation + +class StreamableHttpServerTransportTest { + companion object { + @JvmStatic + @MethodSource + fun invalidPayloads() = listOf( + "", + " ", + " \n \t ", + null, + "lolol", + ) + + private val sizeTestPayload = "x".repeat(64) + + @JvmStatic + fun maxBodySizeTestCases(): List = listOf( + Arguments.of(sizeTestPayload.length.toLong() - 1, HttpStatusCode.PayloadTooLarge), + Arguments.of(sizeTestPayload.length.toLong(), HttpStatusCode.BadRequest), + Arguments.of(sizeTestPayload.length.toLong() + 1, HttpStatusCode.BadRequest), + ) + + @JvmStatic + fun postAcceptHeaderCases(): List = listOf( + Arguments.of("application/json, text/event-stream", HttpStatusCode.OK), + Arguments.of("Application/JSON;q=0.9, TEXT/Event-Stream;charset=utf-8", HttpStatusCode.OK), + Arguments.of("*/*", HttpStatusCode.OK), + Arguments.of("application/*, text/*", HttpStatusCode.OK), + Arguments.of(null, HttpStatusCode.OK), + Arguments.of("application/json", HttpStatusCode.NotAcceptable), + Arguments.of("text/event-stream", HttpStatusCode.NotAcceptable), + Arguments.of("application/jsonp, text/event-stream-bogus", HttpStatusCode.NotAcceptable), + Arguments.of("application/json, text/event-stream;q=0", HttpStatusCode.NotAcceptable), + ) + + @JvmStatic + fun getAcceptHeaderCases() = listOf( + "application/json", + "text/event-stream-bogus", + "text/event-stream;q=0", + ) + } + + private val path = "/transport" + + @Test + fun `POST without event-stream accept header is rejected`() = testApplication { + configTestServer() + + val client = createTestClient() + + val transport = StreamableHttpServerTransport(enableJsonResponse = true) + val onMessageCalled = AtomicBoolean(false) + transport.onMessage { + onMessageCalled.set(true) + } + + configureTransportEndpoint(transport) + + val payload = buildInitializeRequestPayload() + + val response = client.post(path) { + contentType(ContentType.Application.Json) + header(HttpHeaders.Accept, ContentType.Application.Json.toString()) + setBody(payload) + } + + assertEquals(HttpStatusCode.NotAcceptable, response.status) + assertFalse(onMessageCalled.get(), "Transport should not deliver messages when headers are invalid") + } + + @ParameterizedTest + @MethodSource("postAcceptHeaderCases") + fun `POST Accept header is matched as media ranges`(acceptHeader: String?, expectedStatus: HttpStatusCode) = + testApplication { + configTestServer() + + // Bare client: the ContentNegotiation plugin would supply an Accept header of its own, + // leaving the absent-header case untestable. + val client = createClient {} + + val transport = StreamableHttpServerTransport(enableJsonResponse = true) + val onMessageCalled = AtomicBoolean(false) + transport.onMessage { message -> + onMessageCalled.set(true) + if (message is JSONRPCRequest) { + transport.send(JSONRPCResponse(message.id, EmptyResult())) + } + } + + configureTransportEndpoint(transport) + + val response = client.post(path) { + contentType(ContentType.Application.Json) + acceptHeader?.let { header(HttpHeaders.Accept, it) } + setBody(McpJson.encodeToString(JSONRPCMessage.serializer(), buildInitializeRequestPayload())) + } + + assertEquals(expectedStatus, response.status) + assertEquals( + expectedStatus == HttpStatusCode.OK, + onMessageCalled.get(), + "Only a request that clears the Accept gate may reach the transport", + ) + } + + @Test + fun `POST with an unparsable Accept header is rejected without failing the request`() = testApplication { + configTestServer() + + val client = createClient {} + + val transport = StreamableHttpServerTransport(enableJsonResponse = true) + val onMessageCalled = AtomicBoolean(false) + transport.onMessage { + onMessageCalled.set(true) + } + + configureTransportEndpoint(transport) + + val response = client.post(path) { + contentType(ContentType.Application.Json) + header(HttpHeaders.Accept, "not-a-media-type") + setBody(McpJson.encodeToString(JSONRPCMessage.serializer(), buildInitializeRequestPayload())) + } + + // The exact code is decided by content negotiation once the transport has rejected the request. + assertTrue(response.status.value in 400..499, "Expected a client error, got ${response.status}") + assertFalse(onMessageCalled.get(), "Transport should not deliver messages when headers are invalid") + } + + @ParameterizedTest + @MethodSource("getAcceptHeaderCases") + fun `GET whose Accept header admits no event-stream range is rejected`(acceptHeader: String) = testApplication { + configTestServer() + + val client = createClient {} + + val transport = StreamableHttpServerTransport(enableJsonResponse = true) + + application { + routing { + get(path) { + transport.handleGetRequest(FakeServerSSESession(call, call.coroutineContext), call) + } + } + } + + val response = client.get(path) { + header(HttpHeaders.Accept, acceptHeader) + } + + assertEquals(HttpStatusCode.NotAcceptable, response.status) + } + + @Test + fun `initialization request establishes session and returns json response`() = testApplication { + configTestServer() + + val client = createTestClient() + + val transport = StreamableHttpServerTransport(enableJsonResponse = true) + val expectedSessionId = "session-test-id" + transport.setSessionIdGenerator { expectedSessionId } + + var observedRequest: JSONRPCRequest? = null + transport.onMessage { message -> + if (message is JSONRPCRequest) { + observedRequest = message + transport.send(JSONRPCResponse(message.id, EmptyResult()), null) + } + } + + configureTransportEndpoint(transport) + + val payload = buildInitializeRequestPayload() + + val response = client.post(path) { + addStreamableHeaders() + setBody(payload) + } + + assertEquals(HttpStatusCode.OK, response.status) + assertEquals(expectedSessionId, response.headers[MCP_SESSION_ID_HEADER]) + val request = assertNotNull(observedRequest, "Initialization request should be forwarded") + + response.body() shouldBe JSONRPCResponse(request.id) + } + + @Test + fun `second initialization request returns JSON-RPC error with request id`() = testApplication { + configTestServer() + + val client = createTestClient() + + val transport = StreamableHttpServerTransport(enableJsonResponse = true) + transport.onMessage { message -> + if (message is JSONRPCRequest) { + transport.send(JSONRPCResponse(message.id, EmptyResult())) + } + } + + configureTransportEndpoint(transport) + + val payload = buildInitializeRequestPayload() + + val firstResponse = client.post(path) { + addStreamableHeaders() + setBody(payload) + } + + firstResponse.status shouldBe HttpStatusCode.OK + + val secondRequest = buildInitializeRequestPayload().copy(id = RequestId("second-init")) + val secondResponse = client.post(path) { + addStreamableHeaders() + header("mcp-session-id", firstResponse.headers[MCP_SESSION_ID_HEADER]) + setBody(secondRequest) + } + + secondResponse.status shouldBe HttpStatusCode.BadRequest + val error = secondResponse.body() + error.id shouldBe secondRequest.id + error.error.message shouldBe "Invalid Request: Server already initialized" + } + + @Test + fun `init request with unsupported protocol version returns an HTTP error`() = testApplication { + configTestServer() + + val client = createTestClient() + + val transport = StreamableHttpServerTransport(enableJsonResponse = true) + transport.onMessage { message -> + if (message is JSONRPCRequest) { + transport.send(JSONRPCResponse(message.id, EmptyResult())) + } + } + + configureTransportEndpoint(transport) + + val payload = buildInitializeRequestPayload() + val initResponse = client.post(path) { + addStreamableHeaders() + header("mcp-protocol-version", "1900-01-01") + setBody(payload) + } + + initResponse.status shouldBe HttpStatusCode.BadRequest + initResponse.headers[MCP_SESSION_ID_HEADER] shouldBe null + initResponse.body().id shouldBe payload.id + } + + @Test + fun `malformed initialize params return invalid params over streamable http`() = testApplication { + val mcpPath = "/mcp" + + application { + mcpStreamableHttp(mcpPath, enableDnsRebindingProtection = false) { + Server( + Implementation("test-server", "1.0.0"), + ServerOptions(capabilities = ServerCapabilities()), + ) + } + } + + val client = createTestClient() + + val response = client.post(mcpPath) { + addStreamableHeaders() + setBody( + """ + {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{},"clientInfo":{"name":"repro","version":"0.1.0"}}} + """.trimIndent(), + ) + } + + response.status shouldBe HttpStatusCode.OK + val error = response.body() + error.error.code shouldBe RPCError.ErrorCode.INVALID_PARAMS + error.error.message.contains("kotlinx.serialization") shouldBe false + } + + @Test + fun `request with unsupported protocol version returns an HTTP error`() = testApplication { + configTestServer() + + val client = createTestClient() + + val transport = StreamableHttpServerTransport(enableJsonResponse = true) + transport.onMessage { message -> + if (message is JSONRPCRequest) { + transport.send(JSONRPCResponse(message.id, EmptyResult())) + } + } + + configureTransportEndpoint(transport) + + val initResponse = client.post(path) { + addStreamableHeaders() + setBody(buildInitializeRequestPayload()) + } + + initResponse.status shouldBe HttpStatusCode.OK + val sessionId = initResponse.headers[MCP_SESSION_ID_HEADER] + assertNotNull(sessionId) + + val response = client.post(path) { + addStreamableHeaders() + header("mcp-session-id", sessionId) + header("mcp-protocol-version", "1900-01-01") + setBody( + encodeMessages( + listOf( + JSONRPCRequest( + id = RequestId("test-1"), + method = Method.Defined.ToolsList.value, + ), + ), + ), + ) + } + + response.status shouldBe HttpStatusCode.BadRequest + response.body().id shouldBe RequestId("test-1") + } + + @Test + fun `batch with an initialization request echoes the initialize id when rejected`() = testApplication { + configTestServer() + + val client = createTestClient() + + val transport = StreamableHttpServerTransport(enableJsonResponse = true) + transport.onMessage { } + + configureTransportEndpoint(transport) + + val initPayload = buildInitializeRequestPayload() + val response = client.post(path) { + addStreamableHeaders() + setBody( + encodeMessages( + listOf( + initPayload, + JSONRPCRequest(id = RequestId("extra"), method = Method.Defined.ToolsList.value), + ), + ), + ) + } + + response.status shouldBe HttpStatusCode.BadRequest + val error = response.body() + error.error.message shouldBe "Invalid Request: Only one initialization request is allowed" + error.id shouldBe initPayload.id + } + + @Test + fun `non-init request before initialization echoes the request id`() = testApplication { + configTestServer() + + val client = createTestClient() + + val transport = StreamableHttpServerTransport(enableJsonResponse = true) + transport.onMessage { } + + configureTransportEndpoint(transport) + + val response = client.post(path) { + addStreamableHeaders() + setBody( + encodeMessages( + listOf( + JSONRPCRequest(id = RequestId("before-init"), method = Method.Defined.ToolsList.value), + ), + ), + ) + } + + response.status shouldBe HttpStatusCode.BadRequest + val error = response.body() + error.error.message shouldBe "Bad Request: Server not initialized" + error.id shouldBe RequestId("before-init") + } + + @Test + fun `notifications only payload is accepted`() = testApplication { + configTestServer() + + val client = createTestClient() + + val transport = StreamableHttpServerTransport(enableJsonResponse = true) + val receivedMessages = mutableListOf() + transport.onMessage { message -> + if (message is JSONRPCRequest) { + transport.send(JSONRPCResponse(message.id, EmptyResult())) + } + receivedMessages.add(message) + } + + configureTransportEndpoint(transport) + + val initRequest = buildInitializeRequestPayload() + + val responseInit = client.post(path) { + addStreamableHeaders() + setBody(initRequest) + } + + val notificationPayload = encodeMessages( + listOf(InitializedNotification().toJSON()), + ) + + val response = client.post(path) { + addStreamableHeaders() + header("mcp-session-id", responseInit.headers[MCP_SESSION_ID_HEADER]) + setBody(notificationPayload) + } + + assertEquals(HttpStatusCode.Accepted, response.status) + receivedMessages shouldBeEqual listOf(initRequest, InitializedNotification().toJSON()) + } + + @Test + fun `batched requests wait for all responses before replying`() = testApplication { + configTestServer() + + val client = createTestClient(logging = true) + + val transport = StreamableHttpServerTransport(enableJsonResponse = true) + val firstRequest = JSONRPCRequest(id = RequestId("first"), method = Method.Defined.ToolsList.value) + val secondRequest = JSONRPCRequest(id = RequestId("second"), method = Method.Defined.ResourcesList.value) + + val firstResult = ListToolsResult( + tools = listOf( + Tool(name = "tool-1", inputSchema = ToolSchema()), + ), + meta = buildJsonObject { put("label", "first") }, + ) + val secondResult = ListResourcesResult( + resources = emptyList(), + meta = buildJsonObject { put("label", "second") }, + ) + + transport.onMessage { message -> + if (message is JSONRPCRequest) { + val result = when (message.id) { + firstRequest.id -> firstResult + secondRequest.id -> secondResult + else -> EmptyResult() + } + transport.send(JSONRPCResponse(message.id, result), null) + } + } + + configureTransportEndpoint(transport) + + val initRequest = buildInitializeRequestPayload() + + val responseInit = client.post(path) { + addStreamableHeaders() + setBody(initRequest) + } + + val payload = encodeMessages(listOf(firstRequest, secondRequest)) + + val response = client.post(path) { + addStreamableHeaders() + header("mcp-session-id", responseInit.headers[MCP_SESSION_ID_HEADER]) + setBody(payload) + } + + assertEquals(HttpStatusCode.OK, response.status) + + val responses = response.body>() + val results = responses.map { it.result } + results.shouldContainAll(firstResult, secondResult) + + // Check responses' order + + // TODO Uncomment when fixed https://github.com/modelcontextprotocol/kotlin-sdk/issues/548 + /*assertEquals(listOf(firstRequest.id, secondRequest.id), responses.map { it.id }) + val firstMeta = (responses[0] as ListToolsResult).meta + val secondMeta = (responses[1] as ListResourcesResult).meta + assertEquals("first", firstMeta?.get("label")?.jsonPrimitive?.content) + assertEquals("second", secondMeta?.get("label")?.jsonPrimitive?.content)*/ + } + + @Test + fun `json response waits for a response produced after message delivery returns`() = testApplication { + configTestServer() + + val client = createTestClient() + + val transport = StreamableHttpServerTransport( + StreamableHttpServerTransport.Configuration(enableJsonResponse = true), + ) + // Simulates concurrent dispatch: message delivery returns before the handler responds. + val handlerScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + try { + transport.onMessage { message -> + if (message is JSONRPCRequest) { + if (message.method == Method.Defined.Initialize.value) { + transport.send(JSONRPCResponse(message.id, EmptyResult())) + } else { + handlerScope.launch { + delay(100) + transport.send(JSONRPCResponse(message.id, EmptyResult())) + } + } + } + } + + configureTransportEndpoint(transport) + + val initResponse = client.post(path) { + addStreamableHeaders() + setBody(buildInitializeRequestPayload()) + } + initResponse.status shouldBe HttpStatusCode.OK + + val request = JSONRPCRequest(id = RequestId("async-1"), method = Method.Defined.ToolsList.value) + val response = withTimeout(10.seconds) { + client.post(path) { + addStreamableHeaders() + header(MCP_SESSION_ID_HEADER, initResponse.headers[MCP_SESSION_ID_HEADER]) + setBody(encodeMessages(listOf(request))) + } + } + + response.status shouldBe HttpStatusCode.OK + response.body() shouldBe JSONRPCResponse(request.id) + } finally { + handlerScope.cancel() + } + } + + @Test + fun `json response for a batch waits until every asynchronously produced response is ready`() = testApplication { + configTestServer() + + val client = createTestClient() + + val transport = StreamableHttpServerTransport( + StreamableHttpServerTransport.Configuration(enableJsonResponse = true), + ) + val firstRequest = JSONRPCRequest(id = RequestId("slow"), method = Method.Defined.ToolsList.value) + val secondRequest = JSONRPCRequest(id = RequestId("fast"), method = Method.Defined.ResourcesList.value) + + // Simulates concurrent dispatch with out-of-order completion: the second request responds + // first; the POST must stay open until the slower first response settles too. + val handlerScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + try { + transport.onMessage { message -> + if (message is JSONRPCRequest) { + if (message.method == Method.Defined.Initialize.value) { + transport.send(JSONRPCResponse(message.id, EmptyResult())) + } else { + handlerScope.launch { + delay(if (message.id == firstRequest.id) 200 else 50) + transport.send(JSONRPCResponse(message.id, EmptyResult())) + } + } + } + } + + configureTransportEndpoint(transport) + + val initResponse = client.post(path) { + addStreamableHeaders() + setBody(buildInitializeRequestPayload()) + } + initResponse.status shouldBe HttpStatusCode.OK + + val response = withTimeout(10.seconds) { + client.post(path) { + addStreamableHeaders() + header(MCP_SESSION_ID_HEADER, initResponse.headers[MCP_SESSION_ID_HEADER]) + setBody(encodeMessages(listOf(firstRequest, secondRequest))) + } + } + + response.status shouldBe HttpStatusCode.OK + val responses = response.body>() + responses.map { it.id }.toSet() shouldBe setOf(firstRequest.id, secondRequest.id) + } finally { + handlerScope.cancel() + } + } + + @Test + fun `json response POST completes with 202 when its only request is cancelled`() = testApplication { + configTestServer() + + val client = createTestClient() + + val transport = StreamableHttpServerTransport( + StreamableHttpServerTransport.Configuration(enableJsonResponse = true), + ) + val requestDelivered = CompletableDeferred() + // Concurrent dispatch: delivery returns before a response is produced. This request is never + // answered here, modelling a handler cancelled by notifications/cancelled. + transport.onMessage { message -> + if (message is JSONRPCRequest) { + if (message.method == Method.Defined.Initialize.value) { + transport.send(JSONRPCResponse(message.id, EmptyResult())) + } else { + requestDelivered.complete(Unit) + } + } + } + + configureTransportEndpoint(transport) + + val clientScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + try { + val initResponse = client.post(path) { + addStreamableHeaders() + setBody(buildInitializeRequestPayload()) + } + initResponse.status shouldBe HttpStatusCode.OK + val sessionId = initResponse.headers[MCP_SESSION_ID_HEADER] + + val request = JSONRPCRequest(id = RequestId("cancel-me"), method = Method.Defined.ToolsList.value) + val requestPost = clientScope.async { + client.post(path) { + addStreamableHeaders() + header(MCP_SESSION_ID_HEADER, sessionId) + setBody(encodeMessages(listOf(request))) + } + } + + // The request must be registered in-flight before it can be cancelled. + withTimeout(5.seconds) { requestDelivered.await() } + + val cancellation = CancelledNotification( + CancelledNotificationParams(requestId = request.id, reason = "client cancelled"), + ) + val cancelResponse = client.post(path) { + addStreamableHeaders() + header(MCP_SESSION_ID_HEADER, sessionId) + setBody(encodeMessages(listOf(cancellation.toJSON()))) + } + cancelResponse.status shouldBe HttpStatusCode.Accepted + + // Without retirement of the cancelled id this await never completes and the timeout fires. + val response = withTimeout(10.seconds) { requestPost.await() } + response.status shouldBe HttpStatusCode.Accepted + } finally { + clientScope.cancel() + } + } + + @Test + fun `json response for a batch returns only the non-cancelled responses`() = testApplication { + configTestServer() + + val client = createTestClient() + + val transport = StreamableHttpServerTransport( + StreamableHttpServerTransport.Configuration(enableJsonResponse = true), + ) + val answeredA = JSONRPCRequest(id = RequestId("a"), method = Method.Defined.ToolsList.value) + val answeredB = JSONRPCRequest(id = RequestId("b"), method = Method.Defined.ToolsList.value) + val cancelledC = JSONRPCRequest(id = RequestId("c"), method = Method.Defined.ToolsList.value) + + val cancelledDelivered = CompletableDeferred() + val handlerScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + try { + transport.onMessage { message -> + if (message is JSONRPCRequest) { + when { + message.method == Method.Defined.Initialize.value -> + transport.send(JSONRPCResponse(message.id, EmptyResult())) + + message.id == answeredA.id || message.id == answeredB.id -> + handlerScope.launch { transport.send(JSONRPCResponse(message.id, EmptyResult())) } + + message.id == cancelledC.id -> cancelledDelivered.complete(Unit) + } + } + } + + configureTransportEndpoint(transport) + + val initResponse = client.post(path) { + addStreamableHeaders() + setBody(buildInitializeRequestPayload()) + } + initResponse.status shouldBe HttpStatusCode.OK + val sessionId = initResponse.headers[MCP_SESSION_ID_HEADER] + + val batchPost = handlerScope.async { + client.post(path) { + addStreamableHeaders() + header(MCP_SESSION_ID_HEADER, sessionId) + setBody(encodeMessages(listOf(answeredA, answeredB, cancelledC))) + } + } + + withTimeout(5.seconds) { cancelledDelivered.await() } + + val cancellation = CancelledNotification(CancelledNotificationParams(requestId = cancelledC.id)) + client.post(path) { + addStreamableHeaders() + header(MCP_SESSION_ID_HEADER, sessionId) + setBody(encodeMessages(listOf(cancellation.toJSON()))) + }.status shouldBe HttpStatusCode.Accepted + + val response = withTimeout(10.seconds) { batchPost.await() } + response.status shouldBe HttpStatusCode.OK + val responses = response.body>() + responses.map { it.id }.toSet() shouldBe setOf(answeredA.id, answeredB.id) + } finally { + handlerScope.cancel() + } + } + + @Test + fun `json response send for a retired request id is dropped without error`() = runTest { + val transport = StreamableHttpServerTransport(enableJsonResponse = true) + // No stream is mapped for this id (already retired by disconnect/cancellation cleanup); + // a late terminal response must be dropped quietly, not raise "No connection established". + transport.send(JSONRPCResponse(RequestId("retired"), EmptyResult())) + } + + @Test + fun `sse response send for an unknown request id still errors`() = runTest { + val transport = StreamableHttpServerTransport(enableJsonResponse = false) + assertFailsWith { + transport.send(JSONRPCResponse(RequestId("unknown"), EmptyResult())) + } + } + + @Test + fun `sse terminal response is dropped without leaking when the per-request stream is gone`() = testApplication { + configTestServer() + val client = createTestClient() + + val transport = StreamableHttpServerTransport( + StreamableHttpServerTransport.Configuration(enableJsonResponse = false), + ) + transport.setSessionIdGenerator(null) // stateless: accept requests without an init handshake + val requestId = RequestId("held-request") + val delivered = CompletableDeferred() + val release = CompletableDeferred() + transport.onMessage { message -> + if (message is JSONRPCRequest) { + delivered.complete(Unit) + release.await() // keep the POST and its stream mapping alive until the test releases it + } + } + + application { + routing { + post(path) { + transport.handlePostRequest(FakeServerSSESession(call, call.coroutineContext), call) + } + } + } + + val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + try { + val post = scope.async { + client.post(path) { + addStreamableHeaders() + setBody( + encodeMessages( + listOf(JSONRPCRequest(id = requestId, method = Method.Defined.ToolsList.value)), + ), + ) + } + } + withTimeout(5.seconds) { delivered.await() } + + // Drop the per-request SSE stream while the request is still mapped in-flight; the handler's + // terminal response is now undeliverable and must be dropped quietly rather than raise + // "No connection established" (which would also strand the request mappings). + transport.closeSseStream(requestId) + transport.send(JSONRPCResponse(requestId, EmptyResult())) + + post.cancel() + } finally { + release.complete(Unit) + scope.cancel() + } + } + + @ParameterizedTest + @MethodSource("invalidPayloads") + fun `POST with a null or empty body returns an error`(payload: String?) = testApplication { + configTestServer() + + val client = createTestClient() + + val transport = StreamableHttpServerTransport(enableJsonResponse = true) + transport.onMessage { message -> + if (message is JSONRPCRequest) { + transport.send(JSONRPCResponse(message.id, EmptyResult())) + } + } + + configureTransportEndpoint(transport) + + val response = client.post(path) { + addStreamableHeaders() + setBody(payload) + } + + response.status shouldBe HttpStatusCode.BadRequest + } + + @Test + fun `POST with payload at max size is accepted`() = testApplication { + configTestServer() + + val client = createTestClient() + + val transport = StreamableHttpServerTransport(enableJsonResponse = true) + transport.onMessage { message -> + if (message is JSONRPCRequest) { + transport.send(JSONRPCResponse(message.id, EmptyResult())) + } + } + + configureTransportEndpoint(transport) + + val maxSizePayload = "x".repeat(4 * 1024 * 1024) + + val response = client.post(path) { + addStreamableHeaders() + setBody(maxSizePayload) + } + + response.status shouldBe HttpStatusCode.BadRequest + } + + @Test + fun `POST with oversized body returns an error`() = testApplication { + configTestServer() + + val client = createTestClient() + + val transport = StreamableHttpServerTransport(enableJsonResponse = true) + transport.onMessage { message -> + if (message is JSONRPCRequest) { + transport.send(JSONRPCResponse(message.id, EmptyResult())) + } + } + + configureTransportEndpoint(transport) + + val oversizedPayload = "x".repeat(4 * 1024 * 1024 + 1) + + val response = client.post(path) { + addStreamableHeaders() + setBody(oversizedPayload) + } + + response.status shouldBe HttpStatusCode.PayloadTooLarge + } + + @Test + fun `POST with oversized chunked body and no Content-Length returns 413`() = testApplication { + configTestServer() + + val client = createTestClient() + + val transport = StreamableHttpServerTransport( + StreamableHttpServerTransport.Configuration(enableJsonResponse = true, maxRequestBodySize = 1024), + ) + transport.onMessage { message -> + if (message is JSONRPCRequest) { + transport.send(JSONRPCResponse(message.id, EmptyResult())) + } + } + + configureTransportEndpoint(transport) + + // Stream the body as a channel so the client uses chunked transfer-encoding with no + // Content-Length header: the size limit must hold without trusting the (absent) header. + val oversized = ByteReadChannel("x".repeat(4096).encodeToByteArray()) + val response = client.post(path) { + addStreamableHeaders() + setBody(oversized) + } + + response.status shouldBe HttpStatusCode.PayloadTooLarge + } + + @ParameterizedTest + @MethodSource("maxBodySizeTestCases") + fun `POST with custom max request body size validates payload size`( + maxSize: Long, + expectedStatus: HttpStatusCode, + ) = testApplication { + configTestServer() + + val client = createTestClient() + + val transport = StreamableHttpServerTransport( + StreamableHttpServerTransport.Configuration( + enableJsonResponse = true, + maxRequestBodySize = maxSize, + ), + ) + transport.onMessage { message -> + if (message is JSONRPCRequest) { + transport.send(JSONRPCResponse(message.id, EmptyResult())) + } + } + + configureTransportEndpoint(transport) + + val response = client.post(path) { + addStreamableHeaders() + setBody(sizeTestPayload) + } + + response.status shouldBe expectedStatus + } + + @Test + fun `Configuration with negative maxRequestBodySize throws IllegalArgumentException`() { + assertFailsWith { + StreamableHttpServerTransport.Configuration(maxRequestBodySize = -1) + } + } + + @Test + fun `second concurrent GET SSE closes old stream and takes over`() = testApplication { + val mcpPath = "/mcp" + + application { + mcpStreamableHttp(mcpPath) { + Server( + Implementation("test-server", "1.0.0"), + ServerOptions(capabilities = ServerCapabilities()), + ) + } + } + + val client = createTestClient() + + // Step 1: Initialize session via POST + val initResponse = client.post(mcpPath) { + header(HttpHeaders.Host, "localhost") + addStreamableHeaders() + setBody(buildInitializeRequestPayload()) + } + initResponse.status shouldBe HttpStatusCode.OK + val sessionId = assertNotNull(initResponse.headers[MCP_SESSION_ID_HEADER]) + + // Step 2: Open first GET SSE stream + client.prepareGet(mcpPath) { + header(HttpHeaders.Host, "localhost") + header(HttpHeaders.Accept, ContentType.Text.EventStream.toString()) + header(MCP_SESSION_ID_HEADER, sessionId) + header("mcp-protocol-version", LATEST_PROTOCOL_VERSION) + }.execute { firstResponse -> + firstResponse.status shouldBe HttpStatusCode.OK + firstResponse.bodyAsChannel().readUTF8Line() + + // Step 3: Open a second GET — the transport closes the old session + // and the new stream takes over. + client.prepareGet(mcpPath) { + header(HttpHeaders.Host, "localhost") + header(HttpHeaders.Accept, ContentType.Text.EventStream.toString()) + header(MCP_SESSION_ID_HEADER, sessionId) + header("mcp-protocol-version", LATEST_PROTOCOL_VERSION) + }.execute { secondResponse -> + secondResponse.status shouldBe HttpStatusCode.OK + secondResponse.headers[MCP_SESSION_ID_HEADER] shouldBe sessionId + + // New stream is alive + val secondChannel = secondResponse.bodyAsChannel() + val firstLine = secondChannel.readLine() + firstLine.shouldNotBeNull() + secondChannel.isClosedForRead shouldBe false + } + } + } + + @Test + fun `GET SSE reconnect after previous stream disconnects should succeed`() = testApplication { + val mcpPath = "/mcp" + + application { + mcpStreamableHttp(mcpPath) { + Server( + Implementation("test-server", "1.0.0"), + ServerOptions(capabilities = ServerCapabilities()), + ) + } + } + + val client = createTestClient() + + // Step 1: Initialize session via POST + val initResponse = client.post(mcpPath) { + header(HttpHeaders.Host, "localhost") + addStreamableHeaders() + setBody(buildInitializeRequestPayload()) + } + initResponse.status shouldBe HttpStatusCode.OK + val sessionId = assertNotNull(initResponse.headers[MCP_SESSION_ID_HEADER]) + + // Step 2: Open and then close a GET SSE stream + client.prepareGet(mcpPath) { + header(HttpHeaders.Host, "localhost") + header(HttpHeaders.Accept, ContentType.Text.EventStream.toString()) + header(MCP_SESSION_ID_HEADER, sessionId) + header("mcp-protocol-version", LATEST_PROTOCOL_VERSION) + }.execute { response -> + response.status shouldBe HttpStatusCode.OK + response.bodyAsChannel().readLine() + } + + // Step 3: Immediately reconnect — the transport should close the stale + // stream and allow the new one. + client.prepareGet(mcpPath) { + header(HttpHeaders.Host, "localhost") + header(HttpHeaders.Accept, ContentType.Text.EventStream.toString()) + header(MCP_SESSION_ID_HEADER, sessionId) + header("mcp-protocol-version", LATEST_PROTOCOL_VERSION) + }.execute { response -> + response.status shouldBe HttpStatusCode.OK + response.headers[MCP_SESSION_ID_HEADER] shouldBe sessionId + + val channel = response.bodyAsChannel() + val firstLine = channel.readLine() + firstLine.shouldNotBeNull() + channel.isClosedForRead shouldBe false + } + } + + @Test + fun `GET SSE stream includes Mcp-Session-Id header and stays open`() = testApplication { + val mcpPath = "/mcp" + + application { + mcpStreamableHttp(mcpPath) { + Server( + Implementation("test-server", "1.0.0"), + ServerOptions(capabilities = ServerCapabilities()), + ) + } + } + + val client = createTestClient() + + // Step 1: Initialize session via POST + val initResponse = client.post(mcpPath) { + header(HttpHeaders.Host, "localhost") + addStreamableHeaders() + setBody(buildInitializeRequestPayload()) + } + initResponse.status shouldBe HttpStatusCode.OK + val sessionId = assertNotNull(initResponse.headers[MCP_SESSION_ID_HEADER]) + + // Step 2: Open GET SSE stream with session ID + client.prepareGet(mcpPath) { + header(HttpHeaders.Host, "localhost") + header(HttpHeaders.Accept, ContentType.Text.EventStream.toString()) + header(MCP_SESSION_ID_HEADER, sessionId) + header("mcp-protocol-version", LATEST_PROTOCOL_VERSION) + }.execute { response -> + // Verify Mcp-Session-Id is present on the SSE response + response.status shouldBe HttpStatusCode.OK + response.headers[MCP_SESSION_ID_HEADER] shouldBe sessionId + + // Verify the stream is alive by reading at least one line (flush event) + val channel = response.bodyAsChannel() + val firstLine = channel.readLine() + firstLine.shouldNotBeNull() + channel.isClosedForRead shouldBe false + } + } + + private fun ApplicationTestBuilder.configureTransportEndpoint(transport: StreamableHttpServerTransport) { + application { + routing { + post(path) { + transport.handlePostRequest(null, call) + } + } + } + } + + private fun HttpRequestBuilder.addStreamableHeaders() { + header( + HttpHeaders.Accept, + listOf(ContentType.Application.Json, ContentType.Text.EventStream).joinToString(", ") { + it.toString() + }, + ) + contentType(ContentType.Application.Json) + } + + private fun buildInitializeRequestPayload(): JSONRPCRequest { + val request = InitializeRequest( + InitializeRequestParams( + protocolVersion = LATEST_PROTOCOL_VERSION, + capabilities = ClientCapabilities(), + clientInfo = Implementation(name = "test-client", version = "1.0.0"), + ), + ).toJSON() + + return request + } + + private fun encodeMessages(messages: List): String = + McpJson.encodeToString(ListSerializer(JSONRPCMessage.serializer()), messages) + + private fun ApplicationTestBuilder.configTestServer() { + application { + install(ServerContentNegotiation) { + json(McpJson) + } + } + } + + private fun ApplicationTestBuilder.createTestClient(logging: Boolean = false): HttpClient = createClient { + install(ClientContentNegotiation) { + json(McpJson) + } + if (logging) { + install(Logging) { + level = LogLevel.ALL + } + } + } +} + +/** No-op [ServerSSESession] double that lets a POST route drive [StreamableHttpServerTransport] in SSE mode. */ +private class FakeServerSSESession( + override val call: ApplicationCall, + override val coroutineContext: CoroutineContext, +) : ServerSSESession { + override suspend fun send(event: ServerSentEvent) {} + override suspend fun close() {} +} diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/utils/ToolNameValidationTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/utils/ToolNameValidationTest.kt new file mode 100644 index 0000000..f6760ee --- /dev/null +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/utils/ToolNameValidationTest.kt @@ -0,0 +1,123 @@ +package io.modelcontextprotocol.kotlin.sdk.server.utils + +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldContain +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.collections.shouldNotBeEmpty +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +class ToolNameValidationTest { + + @ParameterizedTest + @ValueSource( + strings = [ + "getUser", + "get_user_profile", + "user-profile-update", + "admin.tools.list", + "DATA_EXPORT_v2.1", + "a", + "Z", + "0", + "a-b.c_d", + ], + ) + fun `should return no warnings for valid tool names`(name: String) { + validateToolName(name).shouldBeEmpty() + } + + @Test + fun `should return no warnings for max length name`() { + validateToolName("a".repeat(128)).shouldBeEmpty() + } + + @Test + fun `should warn for empty name`() { + val warnings = validateToolName("") + warnings shouldHaveSize 1 + warnings shouldContain "Tool name cannot be empty" + } + + @Test + fun `should warn for name exceeding max length`() { + val warnings = validateToolName("a".repeat(129)) + warnings.shouldNotBeEmpty() + warnings.any { "exceeds maximum length" in it }.shouldBeTrue() + } + + @ParameterizedTest + @ValueSource(strings = ["get user profile", "my tool"]) + fun `should warn for names with spaces`(name: String) { + val warnings = validateToolName(name) + warnings shouldContain "Tool name contains spaces, which may cause parsing issues" + } + + @ParameterizedTest + @ValueSource(strings = ["get,user,profile", "a,b"]) + fun `should warn for names with commas`(name: String) { + val warnings = validateToolName(name) + warnings shouldContain "Tool name contains commas, which may cause parsing issues" + } + + @ParameterizedTest + @ValueSource(strings = ["user/profile/update", "a/b"]) + fun `should warn for names with forward slashes`(name: String) { + val warnings = validateToolName(name) + warnings.any { "invalid characters" in it }.shouldBeTrue() + } + + @ParameterizedTest + @ValueSource(strings = ["user@domain.com", "tool#1", "tool\$name"]) + fun `should warn for names with special characters`(name: String) { + val warnings = validateToolName(name) + warnings.any { "invalid characters" in it }.shouldBeTrue() + warnings shouldContain "Allowed characters are: A-Z, a-z, 0-9, underscore (_), dash (-), and dot (.)" + } + + @Test + fun `should warn for unicode characters`() { + val warnings = validateToolName("user-\u00f1ame") + warnings.any { "invalid characters" in it }.shouldBeTrue() + } + + @Test + fun `should warn for name starting with dash`() { + val warnings = validateToolName("-my-tool") + warnings shouldContain "Tool name starts or ends with a dash, which may cause parsing issues in some contexts" + } + + @Test + fun `should warn for name ending with dash`() { + val warnings = validateToolName("my-tool-") + warnings shouldContain "Tool name starts or ends with a dash, which may cause parsing issues in some contexts" + } + + @Test + fun `should warn for name starting with dot`() { + val warnings = validateToolName(".hidden") + warnings shouldContain "Tool name starts or ends with a dot, which may cause parsing issues in some contexts" + } + + @Test + fun `should warn for name ending with dot`() { + val warnings = validateToolName("config.") + warnings shouldContain "Tool name starts or ends with a dot, which may cause parsing issues in some contexts" + } + + @Test + fun `should produce multiple warnings for multiple issues`() { + val warnings = validateToolName("my tool,name") + warnings.any { "spaces" in it }.shouldBeTrue() + warnings.any { "commas" in it }.shouldBeTrue() + } + + @Test + fun `should not duplicate space or comma in generic invalid characters message`() { + val warnings = validateToolName("my tool") + warnings shouldContain "Tool name contains spaces, which may cause parsing issues" + warnings.none { it.startsWith("Tool name contains invalid characters") }.shouldBeTrue() + } +} diff --git a/kotlin-sdk-server/src/jvmTest/resources/logback-test.xml b/kotlin-sdk-server/src/jvmTest/resources/logback-test.xml new file mode 100644 index 0000000..aa33478 --- /dev/null +++ b/kotlin-sdk-server/src/jvmTest/resources/logback-test.xml @@ -0,0 +1,15 @@ + + + System.err + + [%thread] %level %logger - %msg%n + + + + + + + + + + diff --git a/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt deleted file mode 100644 index 0294620..0000000 --- a/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt +++ /dev/null @@ -1,895 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk.client - -import io.modelcontextprotocol.kotlin.sdk.ClientCapabilities -import io.modelcontextprotocol.kotlin.sdk.CreateElicitationRequest -import io.modelcontextprotocol.kotlin.sdk.CreateElicitationResult -import io.modelcontextprotocol.kotlin.sdk.CreateMessageRequest -import io.modelcontextprotocol.kotlin.sdk.CreateMessageResult -import io.modelcontextprotocol.kotlin.sdk.EmptyJsonObject -import io.modelcontextprotocol.kotlin.sdk.Implementation -import io.modelcontextprotocol.kotlin.sdk.InitializeRequest -import io.modelcontextprotocol.kotlin.sdk.InitializeResult -import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage -import io.modelcontextprotocol.kotlin.sdk.JSONRPCRequest -import io.modelcontextprotocol.kotlin.sdk.JSONRPCResponse -import io.modelcontextprotocol.kotlin.sdk.LATEST_PROTOCOL_VERSION -import io.modelcontextprotocol.kotlin.sdk.ListResourcesRequest -import io.modelcontextprotocol.kotlin.sdk.ListResourcesResult -import io.modelcontextprotocol.kotlin.sdk.ListRootsRequest -import io.modelcontextprotocol.kotlin.sdk.ListToolsRequest -import io.modelcontextprotocol.kotlin.sdk.ListToolsResult -import io.modelcontextprotocol.kotlin.sdk.LoggingLevel -import io.modelcontextprotocol.kotlin.sdk.LoggingMessageNotification -import io.modelcontextprotocol.kotlin.sdk.Method -import io.modelcontextprotocol.kotlin.sdk.Role -import io.modelcontextprotocol.kotlin.sdk.Root -import io.modelcontextprotocol.kotlin.sdk.RootsListChangedNotification -import io.modelcontextprotocol.kotlin.sdk.SUPPORTED_PROTOCOL_VERSIONS -import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities -import io.modelcontextprotocol.kotlin.sdk.TextContent -import io.modelcontextprotocol.kotlin.sdk.Tool -import io.modelcontextprotocol.kotlin.sdk.server.Server -import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions -import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport -import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.TimeoutCancellationException -import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay -import kotlinx.coroutines.joinAll -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.withTimeout -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.put -import kotlinx.serialization.json.putJsonObject -import kotlin.coroutines.cancellation.CancellationException -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertIs -import kotlin.test.assertTrue -import kotlin.test.fail - -class ClientTest { - @Test - fun `should initialize with matching protocol version`() = runTest { - var initialised = false - val clientTransport = object : AbstractTransport() { - override suspend fun start() {} - - override suspend fun send(message: JSONRPCMessage) { - if (message !is JSONRPCRequest) return - initialised = true - val result = InitializeResult( - protocolVersion = LATEST_PROTOCOL_VERSION, - capabilities = ServerCapabilities(), - serverInfo = Implementation( - name = "test", - version = "1.0", - ), - ) - - val response = JSONRPCResponse( - id = message.id, - result = result, - ) - - _onMessage.invoke(response) - } - - override suspend fun close() { - } - } - - val client = Client( - clientInfo = Implementation( - name = "test client", - version = "1.0", - ), - options = ClientOptions( - capabilities = ClientCapabilities( - sampling = EmptyJsonObject, - ), - ), - ) - - client.connect(clientTransport) - assertTrue(initialised) - } - - @Test - fun `should initialize with supported older protocol version`() = runTest { - val oldVersion = SUPPORTED_PROTOCOL_VERSIONS[1] - val clientTransport = object : AbstractTransport() { - override suspend fun start() {} - - override suspend fun send(message: JSONRPCMessage) { - if (message !is JSONRPCRequest) return - check(message.method == Method.Defined.Initialize.value) - - val result = InitializeResult( - protocolVersion = oldVersion, - capabilities = ServerCapabilities(), - serverInfo = Implementation( - name = "test", - version = "1.0", - ), - ) - - val response = JSONRPCResponse( - id = message.id, - result = result, - ) - _onMessage.invoke(response) - } - - override suspend fun close() { - } - } - - val client = Client( - clientInfo = Implementation( - name = "test client", - version = "1.0", - ), - options = ClientOptions( - capabilities = ClientCapabilities( - sampling = EmptyJsonObject, - ), - ), - ) - - client.connect(clientTransport) - assertEquals( - Implementation("test", "1.0"), - client.serverVersion, - ) - } - - @Test - fun `should reject unsupported protocol version`() = runTest { - var closed = false - val clientTransport = object : AbstractTransport() { - override suspend fun start() {} - - override suspend fun send(message: JSONRPCMessage) { - if (message !is JSONRPCRequest) return - check(message.method == Method.Defined.Initialize.value) - - val result = InitializeResult( - protocolVersion = "invalid-version", - capabilities = ServerCapabilities(), - serverInfo = Implementation( - name = "test", - version = "1.0", - ), - ) - - val response = JSONRPCResponse( - id = message.id, - result = result, - ) - - _onMessage.invoke(response) - } - - override suspend fun close() { - closed = true - } - } - - val client = Client( - clientInfo = Implementation( - name = "test client", - version = "1.0", - ), - options = ClientOptions(), - ) - - assertFailsWith("Server's protocol version is not supported: invalid-version") { - client.connect(clientTransport) - } - - assertTrue(closed) - } - - @Test - fun `should reject due to non cancellation exception`() = runTest { - var closed = false - val failingTransport = object : AbstractTransport() { - override suspend fun start() {} - - override suspend fun send(message: JSONRPCMessage) { - if (message !is JSONRPCRequest) return - check(message.method == Method.Defined.Initialize.value) - throw IllegalStateException("Test error") - } - - override suspend fun close() { - closed = true - } - } - - val client = Client( - clientInfo = Implementation( - name = "test client", - version = "1.0", - ), - options = ClientOptions(), - ) - - val exception = assertFailsWith { - client.connect(failingTransport) - } - - assertEquals("Error connecting to transport: Test error", exception.message) - - assertTrue(closed) - } - - @Test - fun `should respect server capabilities`() = runTest { - val serverOptions = ServerOptions( - capabilities = ServerCapabilities( - resources = ServerCapabilities.Resources(null, null), - tools = ServerCapabilities.Tools(null), - ), - ) - val server = Server( - Implementation(name = "test server", version = "1.0"), - serverOptions, - ) - - server.setRequestHandler(Method.Defined.Initialize) { _, _ -> - InitializeResult( - protocolVersion = LATEST_PROTOCOL_VERSION, - capabilities = ServerCapabilities( - resources = ServerCapabilities.Resources(null, null), - tools = ServerCapabilities.Tools(null), - ), - serverInfo = Implementation(name = "test", version = "1.0"), - ) - } - - server.setRequestHandler(Method.Defined.ResourcesList) { _, _ -> - ListResourcesResult(resources = emptyList(), nextCursor = null) - } - - server.setRequestHandler(Method.Defined.ToolsList) { _, _ -> - ListToolsResult(tools = emptyList(), nextCursor = null) - } - - val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() - - val client = Client( - clientInfo = Implementation(name = "test client", version = "1.0"), - options = ClientOptions( - capabilities = ClientCapabilities(sampling = EmptyJsonObject), - ), - ) - - listOf( - launch { - client.connect(clientTransport) - }, - launch { - server.connect(serverTransport) - }, - ).joinAll() - - // Server supports resources and tools, but not prompts - val caps = client.serverCapabilities - assertEquals(ServerCapabilities.Resources(null, null), caps?.resources) - assertEquals(ServerCapabilities.Tools(null), caps?.tools) - assertTrue(caps?.prompts == null) // or check that prompts are absent - - // These should not throw - client.listResources() - client.listTools() - - // This should fail because prompts are not supported - val ex = assertFailsWith { - client.listPrompts() - } - assertTrue(ex.message?.contains("Server does not support prompts") == true) - } - - @Test - fun `should respect client notification capabilities`() = runTest { - val server = Server( - Implementation(name = "test server", version = "1.0"), - ServerOptions(capabilities = ServerCapabilities()), - ) - - val client = Client( - clientInfo = Implementation(name = "test client", version = "1.0"), - options = ClientOptions( - capabilities = ClientCapabilities( - roots = ClientCapabilities.Roots(listChanged = true), - ), - ), - ) - - val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() - - listOf( - launch { - client.connect(clientTransport) - println("Client connected") - }, - launch { - server.connect(serverTransport) - println("Server connected") - }, - ).joinAll() - - // This should not throw because the client supports roots.listChanged - client.sendRootsListChanged() - - // Create a new client without the roots.listChanged capability - val clientWithoutCapability = Client( - clientInfo = Implementation(name = "test client without capability", version = "1.0"), - options = ClientOptions( - capabilities = ClientCapabilities(), - // enforceStrictCapabilities = true // TODO() - ), - ) - - clientWithoutCapability.connect(clientTransport) - // Using the same transport pair might not be realistic - in a real scenario you'd create another pair. - // Adjust if necessary. - - // This should fail - val ex = assertFailsWith { - clientWithoutCapability.sendRootsListChanged() - } - assertTrue(ex.message?.startsWith("Client does not support") == true) - } - - @Test - fun `should respect server notification capabilities`() = runTest { - val server = Server( - Implementation(name = "test server", version = "1.0"), - ServerOptions( - capabilities = ServerCapabilities( - logging = EmptyJsonObject, - resources = ServerCapabilities.Resources(listChanged = true, subscribe = null), - ), - ), - ) - - val client = Client( - clientInfo = Implementation(name = "test client", version = "1.0"), - options = ClientOptions( - capabilities = ClientCapabilities(), - ), - ) - - val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() - - listOf( - launch { - client.connect(clientTransport) - println("Client connected") - }, - launch { - server.connect(serverTransport) - println("Server connected") - }, - ).joinAll() - - // These should not throw - val jsonObject = buildJsonObject { - put("name", "John") - put("age", 30) - put("isStudent", false) - } - server.sendLoggingMessage( - LoggingMessageNotification( - params = LoggingMessageNotification.Params( - level = LoggingLevel.info, - data = jsonObject, - ), - ), - ) - server.sendResourceListChanged() - - // This should fail because the server doesn't have the tools capability - val ex = assertFailsWith { - server.sendToolListChanged() - } - assertTrue(ex.message?.contains("Server does not support notifying of tool list changes") == true) - } - - @Test - fun `should handle client cancelling a request`() = runTest { - val server = Server( - Implementation(name = "test server", version = "1.0"), - ServerOptions( - capabilities = ServerCapabilities( - resources = ServerCapabilities.Resources( - listChanged = null, - subscribe = null, - ), - ), - ), - ) - - val def = CompletableDeferred() - val defTimeOut = CompletableDeferred() - server.setRequestHandler(Method.Defined.ResourcesList) { _, _ -> - // Simulate delay - def.complete(Unit) - try { - delay(1000) - } catch (e: CancellationException) { - defTimeOut.complete(Unit) - throw e - } - ListResourcesResult(resources = emptyList()) - fail("Shouldn't have been called") - } - - val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() - - val client = Client( - clientInfo = Implementation(name = "test client", version = "1.0"), - options = ClientOptions(capabilities = ClientCapabilities()), - ) - - listOf( - launch { - client.connect(clientTransport) - println("Client connected") - }, - launch { - server.connect(serverTransport) - println("Server connected") - }, - ).joinAll() - - val defCancel = CompletableDeferred() - val job = launch { - try { - client.listResources() - } catch (e: CancellationException) { - defCancel.complete(Unit) - assertEquals("Cancelled by test", e.message) - } - } - def.await() - runCatching { job.cancel("Cancelled by test") } - defCancel.await() - defTimeOut.await() - } - - @Test - fun `should handle request timeout`() = runTest { - val server = Server( - Implementation(name = "test server", version = "1.0"), - ServerOptions( - capabilities = ServerCapabilities( - resources = ServerCapabilities.Resources( - listChanged = null, - subscribe = null, - ), - ), - ), - ) - - server.setRequestHandler(Method.Defined.ResourcesList) { _, _ -> - // Simulate a delayed response - // Wait ~100ms unless canceled - try { - withTimeout(100L) { - // Just delay here, if timeout is 0 on the client side, this won't return in time - delay(100) - } - } catch (_: Exception) { - // If aborted, just rethrow or return early - } - ListResourcesResult(resources = emptyList()) - } - - val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() - val client = Client( - clientInfo = Implementation(name = "test client", version = "1.0"), - options = ClientOptions(capabilities = ClientCapabilities()), - ) - - listOf( - launch { - client.connect(clientTransport) - println("Client connected") - }, - launch { - server.connect(serverTransport) - println("Server connected") - }, - ).joinAll() - - // Request with 1 msec timeout should fail immediately - val ex = assertFailsWith { - withTimeout(1) { - client.listResources() - } - } - assertTrue(ex is TimeoutCancellationException) - } - - @Test - fun `should only allow setRequestHandler for declared capabilities`() = runTest { - val client = Client( - clientInfo = Implementation( - name = "test client", - version = "1.0", - ), - options = ClientOptions( - capabilities = ClientCapabilities( - sampling = EmptyJsonObject, - ), - ), - ) - - client.setRequestHandler(Method.Defined.SamplingCreateMessage) { _, _ -> - CreateMessageResult( - model = "test-model", - role = Role.assistant, - content = TextContent( - text = "Test response", - ), - ) - } - - assertFailsWith("Client does not support roots capability (required for RootsList)") { - client.setRequestHandler(Method.Defined.RootsList) { _, _ -> null } - } - } - - @Test - fun `JSONRPCRequest with ToolsList method and default params returns list of tools`() = runTest { - val serverOptions = ServerOptions( - capabilities = ServerCapabilities( - tools = ServerCapabilities.Tools(null), - ), - ) - val server = Server( - Implementation(name = "test server", version = "1.0"), - serverOptions, - ) - - server.setRequestHandler(Method.Defined.Initialize) { _, _ -> - InitializeResult( - protocolVersion = LATEST_PROTOCOL_VERSION, - capabilities = ServerCapabilities( - resources = ServerCapabilities.Resources(null, null), - tools = ServerCapabilities.Tools(null), - ), - serverInfo = Implementation(name = "test", version = "1.0"), - ) - } - val serverListToolsResult = ListToolsResult( - tools = listOf( - Tool( - name = "testTool", - title = "testTool title", - description = "testTool description", - annotations = null, - inputSchema = Tool.Input(), - outputSchema = null, - ), - ), - nextCursor = null, - ) - - server.setRequestHandler(Method.Defined.ToolsList) { _, _ -> - serverListToolsResult - } - - val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() - - val client = Client( - clientInfo = Implementation(name = "test client", version = "1.0"), - options = ClientOptions( - capabilities = ClientCapabilities(sampling = EmptyJsonObject), - ), - ) - - var receivedMessage: JSONRPCMessage? = null - clientTransport.onMessage { msg -> - receivedMessage = msg - } - - listOf( - launch { - client.connect(clientTransport) - }, - launch { - server.connect(serverTransport) - }, - ).joinAll() - - val serverCapabilities = client.serverCapabilities - assertEquals(ServerCapabilities.Tools(null), serverCapabilities?.tools) - - val request = JSONRPCRequest( - method = Method.Defined.ToolsList.value, - ) - clientTransport.send(request) - - assertIs(receivedMessage) - val receivedAsResponse = receivedMessage as JSONRPCResponse - assertEquals(request.id, receivedAsResponse.id) - assertEquals(request.jsonrpc, receivedAsResponse.jsonrpc) - assertEquals(serverListToolsResult, receivedAsResponse.result) - assertEquals(null, receivedAsResponse.error) - } - - @Test - fun `listRoots returns list of roots`() = runTest { - val client = Client( - Implementation(name = "test client", version = "1.0"), - ClientOptions( - capabilities = ClientCapabilities( - roots = ClientCapabilities.Roots(null), - ), - ), - ) - - val clientRoots = listOf( - Root(uri = "file:///test-root", name = "testRoot"), - ) - - client.addRoots(clientRoots) - - val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() - - val server = Server( - serverInfo = Implementation(name = "test server", version = "1.0"), - options = ServerOptions( - capabilities = ServerCapabilities(), - ), - ) - - listOf( - launch { client.connect(clientTransport) }, - launch { server.connect(serverTransport) }, - ).joinAll() - - val clientCapabilities = server.clientCapabilities - assertEquals(ClientCapabilities.Roots(null), clientCapabilities?.roots) - - val listRootsResult = server.listRoots() - - assertEquals(listRootsResult.roots, clientRoots) - } - - @Test - fun `addRoot should throw when roots capability is not supported`() = runTest { - val client = Client( - Implementation(name = "test client", version = "1.0"), - ClientOptions( - capabilities = ClientCapabilities(), - ), - ) - - // Verify that adding a root throws an exception - val exception = assertFailsWith { - client.addRoot(uri = "file:///test-root1", name = "testRoot1") - } - assertEquals("Client does not support roots capability.", exception.message) - } - - @Test - fun `removeRoot should throw when roots capability is not supported`() = runTest { - val client = Client( - Implementation(name = "test client", version = "1.0"), - ClientOptions( - capabilities = ClientCapabilities(), - ), - ) - - // Verify that removing a root throws an exception - val exception = assertFailsWith { - client.removeRoot(uri = "file:///test-root1") - } - assertEquals("Client does not support roots capability.", exception.message) - } - - @Test - fun `removeRoot should remove a root`() = runTest { - val client = Client( - Implementation(name = "test client", version = "1.0"), - ClientOptions( - capabilities = ClientCapabilities( - roots = ClientCapabilities.Roots(null), - ), - ), - ) - - // Add some roots - client.addRoots( - listOf( - Root(uri = "file:///test-root1", name = "testRoot1"), - Root(uri = "file:///test-root2", name = "testRoot2"), - ), - ) - - // Remove a root - val result = client.removeRoot("file:///test-root1") - - // Verify the root was removed - assertTrue(result, "Root should be removed successfully") - } - - @Test - fun `removeRoots should remove multiple roots`() = runTest { - val client = Client( - Implementation(name = "test client", version = "1.0"), - ClientOptions( - capabilities = ClientCapabilities( - roots = ClientCapabilities.Roots(null), - ), - ), - ) - - // Add some roots - client.addRoots( - listOf( - Root(uri = "file:///test-root1", name = "testRoot1"), - Root(uri = "file:///test-root2", name = "testRoot2"), - ), - ) - - // Remove multiple roots - val result = client.removeRoots( - listOf("file:///test-root1", "file:///test-root2"), - ) - - // Verify the root was removed - assertEquals(2, result, "Both roots should be removed") - } - - @Test - fun `sendRootsListChanged should notify server`() = runTest { - val client = Client( - Implementation(name = "test client", version = "1.0"), - ClientOptions( - capabilities = ClientCapabilities( - roots = ClientCapabilities.Roots(listChanged = true), - ), - ), - ) - - val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() - - val server = Server( - serverInfo = Implementation(name = "test server", version = "1.0"), - options = ServerOptions( - capabilities = ServerCapabilities(), - ), - ) - - // Track notifications - var rootListChangedNotificationReceived = false - server.setNotificationHandler(Method.Defined.NotificationsRootsListChanged) { - rootListChangedNotificationReceived = true - CompletableDeferred(Unit) - } - - listOf( - launch { client.connect(clientTransport) }, - launch { server.connect(serverTransport) }, - ).joinAll() - - client.sendRootsListChanged() - - assertTrue( - rootListChangedNotificationReceived, - "Notification should be sent when sendRootsListChanged is called", - ) - } - - @Test - fun `should reject server elicitation when elicitation capability is not supported`() = runTest { - val client = Client( - Implementation(name = "test client", version = "1.0"), - ClientOptions( - capabilities = ClientCapabilities(), - ), - ) - - val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() - - val server = Server( - serverInfo = Implementation(name = "test server", version = "1.0"), - options = ServerOptions( - capabilities = ServerCapabilities(), - ), - ) - - listOf( - launch { client.connect(clientTransport) }, - launch { server.connect(serverTransport) }, - ).joinAll() - - // Verify that creating an elicitation throws an exception - val exception = assertFailsWith { - server.createElicitation( - message = "Please provide your GitHub username", - requestedSchema = CreateElicitationRequest.RequestedSchema( - properties = buildJsonObject { - putJsonObject("name") { - put("type", "string") - } - }, - required = listOf("name"), - ), - ) - } - assertEquals( - "Client does not support elicitation (required for elicitation/create)", - exception.message, - ) - } - - @Test - fun `should handle server elicitation`() = runTest { - val client = Client( - Implementation(name = "test client", version = "1.0"), - ClientOptions( - capabilities = ClientCapabilities( - elicitation = EmptyJsonObject, - ), - ), - ) - - val elicitationMessage = "Please provide your GitHub username" - val requestedSchema = CreateElicitationRequest.RequestedSchema( - properties = buildJsonObject { - putJsonObject("name") { - put("type", "string") - } - }, - required = listOf("name"), - ) - - val elicitationResultAction = CreateElicitationResult.Action.accept - val elicitationResultContent = buildJsonObject { - put("name", "octocat") - } - - client.setElicitationHandler { request -> - assertEquals(elicitationMessage, request.message) - assertEquals(requestedSchema, request.requestedSchema) - - CreateElicitationResult( - action = elicitationResultAction, - content = elicitationResultContent, - ) - } - - val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() - - val server = Server( - serverInfo = Implementation(name = "test server", version = "1.0"), - options = ServerOptions( - capabilities = ServerCapabilities(), - ), - ) - - listOf( - launch { client.connect(clientTransport) }, - launch { server.connect(serverTransport) }, - ).joinAll() - - val result = server.createElicitation( - message = elicitationMessage, - requestedSchema = requestedSchema, - ) - - assertEquals(elicitationResultAction, result.action) - assertEquals(elicitationResultContent, result.content) - } -} diff --git a/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/InMemoryTransportTest.kt b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/InMemoryTransportTest.kt deleted file mode 100644 index 3e7cdb2..0000000 --- a/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/InMemoryTransportTest.kt +++ /dev/null @@ -1,110 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk.integration - -import io.modelcontextprotocol.kotlin.sdk.InitializedNotification -import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage -import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport -import io.modelcontextprotocol.kotlin.sdk.toJSON -import kotlinx.coroutines.test.runTest -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertNotNull -import kotlin.test.assertTrue - -class InMemoryTransportTest { - private lateinit var clientTransport: InMemoryTransport - private lateinit var serverTransport: InMemoryTransport - - @BeforeTest - fun setUp() { - val (client, server) = InMemoryTransport.createLinkedPair() - clientTransport = client - serverTransport = server - } - - @Test - fun `should create linked pair`() { - assertNotNull(clientTransport) - assertNotNull(serverTransport) - } - - @Test - fun `should start without error`() = runTest { - clientTransport.start() - serverTransport.start() - // If no exception is thrown, the test passes - } - - @Test - fun `should send message from client to server`() = runTest { - val message = InitializedNotification() - - var receivedMessage: JSONRPCMessage? = null - serverTransport.onMessage { msg -> - receivedMessage = msg - } - - val rpcNotification = message.toJSON() - clientTransport.send(rpcNotification) - assertEquals(rpcNotification, receivedMessage) - } - - @Test - fun `should send message from server to client`() = runTest { - val message = InitializedNotification() - .toJSON() - - var receivedMessage: JSONRPCMessage? = null - clientTransport.onMessage { msg -> - receivedMessage = msg - } - - serverTransport.send(message) - assertEquals(message, receivedMessage) - } - - @Test - fun `should handle close`() = runTest { - var clientClosed = false - var serverClosed = false - - clientTransport.onClose { - clientClosed = true - } - - serverTransport.onClose { - serverClosed = true - } - - clientTransport.close() - assertTrue(clientClosed) - assertTrue(serverClosed) - } - - @Test - fun `should throw error when sending after close`() = runTest { - clientTransport.close() - - assertFailsWith { - clientTransport.send( - InitializedNotification().toJSON(), - ) - } - } - - @Test - fun `should queue messages sent before start`() = runTest { - val message = InitializedNotification() - .toJSON() - - var receivedMessage: JSONRPCMessage? = null - serverTransport.onMessage { msg -> - receivedMessage = msg - } - - clientTransport.send(message) - serverTransport.start() - assertEquals(message, receivedMessage) - } -} diff --git a/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/SseIntegrationTest.kt b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/SseIntegrationTest.kt deleted file mode 100644 index 2d1f25c..0000000 --- a/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/SseIntegrationTest.kt +++ /dev/null @@ -1,63 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk.integration - -import io.ktor.client.HttpClient -import io.ktor.server.application.install -import io.ktor.server.cio.CIOApplicationEngine -import io.ktor.server.engine.EmbeddedServer -import io.ktor.server.engine.embeddedServer -import io.ktor.server.routing.routing -import io.modelcontextprotocol.kotlin.sdk.Implementation -import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities -import io.modelcontextprotocol.kotlin.sdk.client.Client -import io.modelcontextprotocol.kotlin.sdk.client.mcpSse -import io.modelcontextprotocol.kotlin.sdk.server.Server -import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions -import io.modelcontextprotocol.kotlin.sdk.server.mcp -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.withContext -import kotlin.test.Test -import kotlin.test.fail -import io.ktor.client.engine.cio.CIO as ClientCIO -import io.ktor.client.plugins.sse.SSE as ClientSSE -import io.ktor.server.cio.CIO as ServerCIO -import io.ktor.server.sse.SSE as ServerSSE - -private const val URL = "127.0.0.1" - -class SseIntegrationTest { - @Test - fun `client should be able to connect to sse server`() = runTest { - val serverEngine = initServer() - try { - withContext(Dispatchers.Default) { - val port = serverEngine.engine.resolvedConnectors().first().port - val client = initClient(port) - client.close() - } - } catch (e: Exception) { - fail("Failed to connect client: $e") - } finally { - // Make sure to stop the server - serverEngine.stopSuspend(1000, 2000) - } - } - - private suspend fun initClient(port: Int): Client = HttpClient(ClientCIO) { - install(ClientSSE) - }.mcpSse("http://$URL:$port") - - private suspend fun initServer(): EmbeddedServer { - val server = Server( - Implementation(name = "sse-e2e-test", version = "1.0.0"), - ServerOptions(capabilities = ServerCapabilities()), - ) - - return embeddedServer(ServerCIO, host = URL, port = 0) { - install(ServerSSE) - routing { - mcp { server } - } - }.startSuspend(wait = false) - } -} diff --git a/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/BaseTransportTest.kt b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/BaseTransportTest.kt deleted file mode 100644 index acb2f27..0000000 --- a/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/BaseTransportTest.kt +++ /dev/null @@ -1,62 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk.shared - -import io.modelcontextprotocol.kotlin.sdk.InitializedNotification -import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage -import io.modelcontextprotocol.kotlin.sdk.PingRequest -import io.modelcontextprotocol.kotlin.sdk.toJSON -import kotlinx.coroutines.CompletableDeferred -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue -import kotlin.test.fail - -abstract class BaseTransportTest { - protected suspend fun testClientOpenClose(client: Transport) { - client.onError { error -> - fail("Unexpected error: $error") - } - - var didClose = false - client.onClose { didClose = true } - - client.start() - assertFalse(didClose, "Transport should not be closed immediately after start") - - client.close() - assertTrue(didClose, "Transport should be closed after close() call") - } - - protected suspend fun testClientRead(client: Transport) { - client.onError { error -> - error.printStackTrace() - fail("Unexpected error: $error") - } - - val messages = listOf( - PingRequest().toJSON(), - InitializedNotification().toJSON(), - ) - - val readMessages = mutableListOf() - val finished = CompletableDeferred() - - client.onMessage { message -> - readMessages.add(message) - if (message == messages.last()) { - finished.complete(Unit) - } - } - - client.start() - - for (message in messages) { - client.send(message) - } - - finished.await() - - assertEquals(messages, readMessages, "Assert messages received") - - client.close() - } -} diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt deleted file mode 100644 index 6d0da32..0000000 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt +++ /dev/null @@ -1,68 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk.client - -import io.modelcontextprotocol.kotlin.sdk.shared.BaseTransportTest -import kotlinx.coroutines.test.runTest -import kotlinx.io.asSink -import kotlinx.io.asSource -import kotlinx.io.buffered -import org.junit.jupiter.api.Test - -class StdioClientTransportTest : BaseTransportTest() { - @Test - fun `should start then close cleanly`() = runTest { - // Run process "/usr/bin/tee" - val processBuilder = ProcessBuilder("/usr/bin/tee") - val process = processBuilder.start() - - val input = process.inputStream.asSource().buffered() - val output = process.outputStream.asSink().buffered() - - val client = StdioClientTransport( - input = input, - output = output, - ) - - testClientOpenClose(client) - - process.destroy() - } - - @Test - fun `should read messages`() = runTest { - val processBuilder = ProcessBuilder("/usr/bin/tee") - val process = processBuilder.start() - - val input = process.inputStream.asSource().buffered() - val output = process.outputStream.asSink().buffered() - - val client = StdioClientTransport( - input = input, - output = output, - ) - - testClientRead(client) - - process.waitFor() - process.destroy() - } - - @Test - fun `should ignore first output messages`() = runTest { - val processBuilder = ProcessBuilder("/usr/bin/tee") - val process = processBuilder.start() - process.outputStream.write("Stdio server started".toByteArray()) - - val input = process.inputStream.asSource().buffered() - val output = process.outputStream.asSink().buffered() - - val client = StdioClientTransport( - input = input, - output = output, - ) - - testClientRead(client) - - process.waitFor() - process.destroy() - } -} diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt deleted file mode 100644 index 563fa85..0000000 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt +++ /dev/null @@ -1,185 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk.integration.kotlin - -import io.ktor.client.HttpClient -import io.ktor.client.engine.cio.CIO -import io.ktor.client.plugins.sse.SSE -import io.ktor.server.application.install -import io.ktor.server.engine.EmbeddedServer -import io.ktor.server.engine.embeddedServer -import io.ktor.server.routing.routing -import io.modelcontextprotocol.kotlin.sdk.Implementation -import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities -import io.modelcontextprotocol.kotlin.sdk.client.Client -import io.modelcontextprotocol.kotlin.sdk.client.SseClientTransport -import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport -import io.modelcontextprotocol.kotlin.sdk.integration.utils.Retry -import io.modelcontextprotocol.kotlin.sdk.server.Server -import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions -import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport -import io.modelcontextprotocol.kotlin.sdk.server.mcp -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withTimeout -import kotlinx.io.Sink -import kotlinx.io.Source -import kotlinx.io.asSink -import kotlinx.io.asSource -import kotlinx.io.buffered -import org.awaitility.kotlin.await -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import java.io.PipedInputStream -import java.io.PipedOutputStream -import kotlin.time.Duration.Companion.seconds -import io.ktor.server.cio.CIO as ServerCIO -import io.ktor.server.sse.SSE as ServerSSE - -@Retry(times = 3) -abstract class KotlinTestBase { - - protected val host = "localhost" - protected var port: Int = 0 - - protected lateinit var server: Server - protected lateinit var client: Client - protected lateinit var serverEngine: EmbeddedServer<*, *> - - // Transport selection - protected enum class TransportKind { SSE, STDIO } - protected open val transportKind: TransportKind = TransportKind.STDIO - - // STDIO-specific fields - private var stdioServerTransport: StdioServerTransport? = null - private var stdioClientInput: Source? = null - private var stdioClientOutput: Sink? = null - - protected abstract fun configureServerCapabilities(): ServerCapabilities - protected abstract fun configureServer() - - @BeforeEach - fun setUp() { - setupServer() - if (transportKind == TransportKind.SSE) { - await - .ignoreExceptions() - .until { - port = runBlocking { serverEngine.engine.resolvedConnectors().first().port } - port != 0 - } - } - runBlocking { - setupClient() - } - } - - protected suspend fun setupClient() { - when (transportKind) { - TransportKind.SSE -> { - val transport = SseClientTransport( - HttpClient(CIO) { - install(SSE) - }, - "http://$host:$port", - ) - client = Client( - Implementation("test", "1.0"), - ) - client.connect(transport) - } - - TransportKind.STDIO -> { - val input = checkNotNull(stdioClientInput) { "STDIO client input not initialized" } - val output = checkNotNull(stdioClientOutput) { "STDIO client output not initialized" } - val transport = StdioClientTransport( - input = input, - output = output, - ) - client = Client( - Implementation("test", "1.0"), - ) - client.connect(transport) - } - } - } - - protected fun setupServer() { - val capabilities = configureServerCapabilities() - - server = Server( - Implementation(name = "test-server", version = "1.0"), - ServerOptions(capabilities = capabilities), - ) - - configureServer() - - if (transportKind == TransportKind.SSE) { - serverEngine = embeddedServer(ServerCIO, host = host, port = port) { - install(ServerSSE) - routing { - mcp { server } - } - }.start(wait = false) - } else { - // Create in-memory stdio pipes: client->server and server->client - val clientToServerOut = PipedOutputStream() - val clientToServerIn = PipedInputStream(clientToServerOut) - - val serverToClientOut = PipedOutputStream() - val serverToClientIn = PipedInputStream(serverToClientOut) - - // Server transport reads from client and writes to client - val serverTransport = StdioServerTransport( - inputStream = clientToServerIn.asSource().buffered(), - outputStream = serverToClientOut.asSink().buffered(), - ) - stdioServerTransport = serverTransport - - // Prepare client-side streams for later client initialization - stdioClientInput = serverToClientIn.asSource().buffered() - stdioClientOutput = clientToServerOut.asSink().buffered() - - // Start server transport by connecting the server - runBlocking { - server.connect(serverTransport) - } - } - } - - @AfterEach - fun tearDown() { - // close client - if (::client.isInitialized) { - try { - runBlocking { - withTimeout(3.seconds) { - client.close() - } - } - } catch (e: Exception) { - println("Warning: Error during client close: ${e.message}") - } - } - - // stop server - if (transportKind == TransportKind.SSE) { - if (::serverEngine.isInitialized) { - try { - serverEngine.stop(500, 1000) - } catch (e: Exception) { - println("Warning: Error during server stop: ${e.message}") - } - } - } else { - stdioServerTransport?.let { - try { - runBlocking { it.close() } - } catch (e: Exception) { - println("Warning: Error during stdio server stop: ${e.message}") - } finally { - stdioServerTransport = null - stdioClientInput = null - stdioClientOutput = null - } - } - } - } -} diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt deleted file mode 100644 index 801f582..0000000 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt +++ /dev/null @@ -1,454 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk.integration.typescript - -import io.ktor.client.HttpClient -import io.ktor.client.engine.cio.CIO -import io.ktor.client.plugins.sse.SSE -import io.modelcontextprotocol.kotlin.sdk.Implementation -import io.modelcontextprotocol.kotlin.sdk.client.Client -import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport -import io.modelcontextprotocol.kotlin.sdk.client.mcpStreamableHttp -import io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse.KotlinServerForTsClient -import io.modelcontextprotocol.kotlin.sdk.integration.utils.Retry -import io.modelcontextprotocol.kotlin.sdk.server.Server -import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport -import kotlinx.coroutines.withTimeout -import kotlinx.io.Sink -import kotlinx.io.Source -import kotlinx.io.asSink -import kotlinx.io.asSource -import kotlinx.io.buffered -import org.awaitility.kotlin.await -import org.junit.jupiter.api.BeforeAll -import java.io.BufferedReader -import java.io.File -import java.io.InputStreamReader -import java.net.ServerSocket -import java.net.Socket -import java.util.concurrent.TimeUnit -import kotlin.io.path.createTempDirectory -import kotlin.time.Duration.Companion.seconds - -enum class TransportKind { SSE, STDIO, DEFAULT } - -@Retry(times = 3) -abstract class TsTestBase { - - protected open val transportKind: TransportKind = TransportKind.DEFAULT - - protected val projectRoot: File get() = File(System.getProperty("user.dir")) - protected val tsClientDir: File - get() { - val base = File( - projectRoot, - "src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript", - ) - - // Allow override via system property for CI: -Dts.transport=stdio|sse - val fromProp = System.getProperty("ts.transport")?.lowercase() - val overrideSubDir = when (fromProp) { - "stdio" -> "stdio" - "sse" -> "sse" - else -> null - } - - val subDirName = overrideSubDir ?: when (transportKind) { - TransportKind.STDIO -> "stdio" - TransportKind.SSE -> "sse" - TransportKind.DEFAULT -> null - } - if (subDirName != null) { - val sub = File(base, subDirName) - if (sub.exists()) return sub - } - return base - } - - companion object { - @JvmStatic - private val tempRootDir: File = createTempDirectory("typescript-sdk-").toFile().apply { deleteOnExit() } - - @JvmStatic - protected val sdkDir: File = File(tempRootDir, "typescript-sdk") - - @JvmStatic - @BeforeAll - fun setupTypeScriptSdk() { - println("Cloning TypeScript SDK repository") - - if (!sdkDir.exists()) { - val process = ProcessBuilder( - "git", - "clone", - "--depth", - "1", - "https://github.com/modelcontextprotocol/typescript-sdk.git", - sdkDir.absolutePath, - ) - .redirectErrorStream(true) - .start() - val exitCode = process.waitFor() - if (exitCode != 0) { - throw RuntimeException("Failed to clone TypeScript SDK repository: exit code $exitCode") - } - } - - println("Installing TypeScript SDK dependencies") - executeCommand("npm install", sdkDir, allowFailure = false, timeoutSeconds = null) - } - - @JvmStatic - protected fun killProcessOnPort(port: Int) { - val isWindows = System.getProperty("os.name").lowercase().contains("windows") - val killCommand = if (isWindows) { - "netstat -ano | findstr :$port | for /f \"tokens=5\" %a in ('more')" + - " do taskkill /F /PID %a 2>nul || echo No process found" - } else { - "lsof -ti:$port | xargs kill -9 2>/dev/null || true" - } - executeCommand(killCommand, File("."), allowFailure = true, timeoutSeconds = null) - } - - @JvmStatic - protected fun findFreePort(): Int { - ServerSocket(0).use { socket -> - return socket.localPort - } - } - - @JvmStatic - protected fun executeCommand( - command: String, - workingDir: File, - allowFailure: Boolean = false, - timeoutSeconds: Long? = null, - ): String { - if (!workingDir.exists()) { - if (!workingDir.mkdirs()) { - throw RuntimeException("Failed to create working directory: ${workingDir.absolutePath}") - } - } - - if (!workingDir.isDirectory || !workingDir.canRead()) { - throw RuntimeException("Working directory is not accessible: ${workingDir.absolutePath}") - } - - val isWindows = System.getProperty("os.name").lowercase().contains("windows") - val processBuilder = if (isWindows) { - ProcessBuilder() - .command("cmd.exe", "/c", "set TYPESCRIPT_SDK_DIR=${sdkDir.absolutePath} && $command") - } else { - ProcessBuilder() - .command("bash", "-c", "TYPESCRIPT_SDK_DIR='${sdkDir.absolutePath}' $command") - } - - val process = processBuilder - .directory(workingDir) - .redirectErrorStream(true) - .start() - - val output = StringBuilder() - BufferedReader(InputStreamReader(process.inputStream)).use { reader -> - var line: String? - while (reader.readLine().also { line = it } != null) { - println(line) - output.append(line).append("\n") - } - } - - if (timeoutSeconds == null) { - val exitCode = process.waitFor() - if (!allowFailure && exitCode != 0) { - throw RuntimeException( - "Command execution failed with exit code $exitCode: $command\n" + - "Working dir: ${workingDir.absolutePath}\nOutput:\n$output", - ) - } - } else { - process.waitFor(timeoutSeconds, TimeUnit.SECONDS) - } - - return output.toString() - } - } - - private fun waitForProcessTermination(process: Process, timeoutSeconds: Long): Boolean { - if (process.isAlive && !process.waitFor(timeoutSeconds, TimeUnit.SECONDS)) { - process.destroyForcibly() - process.waitFor(2, TimeUnit.SECONDS) - return false - } - return true - } - - private fun createProcessOutputReader(process: Process, prefix: String = "TS-SERVER"): Thread { - val outputReader = Thread { - try { - process.inputStream.bufferedReader().useLines { lines -> - for (line in lines) { - println("[$prefix] $line") - } - } - } catch (e: Exception) { - println("Warning: Error reading process output: ${e.message}") - } - } - outputReader.isDaemon = true - return outputReader - } - - private fun createProcessErrorReader(process: Process, prefix: String = "TS-SERVER"): Thread { - val errorReader = Thread { - try { - process.errorStream.bufferedReader().useLines { lines -> - for (line in lines) { - println("[$prefix][err] $line") - } - } - } catch (e: Exception) { - println("Warning: Error reading process error stream: ${e.message}") - } - } - errorReader.isDaemon = true - return errorReader - } - - protected fun waitForPort(host: String = "localhost", port: Int, timeoutSeconds: Long = 10): Boolean = try { - await.atMost(timeoutSeconds, TimeUnit.SECONDS) - .pollDelay(200, TimeUnit.MILLISECONDS) - .pollInterval(100, TimeUnit.MILLISECONDS) - .until { - try { - Socket(host, port).use { true } - } catch (_: Exception) { - false - } - } - true - } catch (_: Exception) { - false - } - - protected fun executeCommandAllowingFailure(command: String, workingDir: File, timeoutSeconds: Long = 20): String = - executeCommand(command, workingDir, allowFailure = true, timeoutSeconds = timeoutSeconds) - - protected fun startTypeScriptServer(port: Int): Process { - killProcessOnPort(port) - - if (!sdkDir.exists() || !sdkDir.isDirectory) { - throw IllegalStateException( - "TypeScript SDK directory does not exist or is not accessible: ${sdkDir.absolutePath}", - ) - } - - val isWindows = System.getProperty("os.name").lowercase().contains("windows") - val localServerPath = File(tsClientDir, "simpleStreamableHttp.ts").absolutePath - val processBuilder = if (isWindows) { - ProcessBuilder() - .command( - "cmd.exe", - "/c", - "set MCP_PORT=$port && set NODE_PATH=${sdkDir.absolutePath}\\node_modules && npx --prefix \"${sdkDir.absolutePath}\" tsx \"$localServerPath\"", - ) - } else { - ProcessBuilder() - .command( - "bash", - "-c", - "MCP_PORT=$port NODE_PATH='${sdkDir.absolutePath}/node_modules' npx --prefix '${sdkDir.absolutePath}' tsx \"$localServerPath\"", - ) - } - - processBuilder.environment()["TYPESCRIPT_SDK_DIR"] = sdkDir.absolutePath - - val process = processBuilder - .directory(tsClientDir) - .redirectErrorStream(true) - .start() - - createProcessOutputReader(process).start() - - if (!waitForPort(port = port, timeoutSeconds = 20)) { - throw IllegalStateException("TypeScript server did not become ready on localhost:$port within timeout") - } - return process - } - - protected fun stopProcess(process: Process, waitSeconds: Long = 3, name: String = "TypeScript server") { - process.destroy() - if (waitForProcessTermination(process, waitSeconds)) { - println("$name stopped gracefully") - } else { - println("$name did not stop gracefully, forced termination") - } - } - - // ===== SSE client helpers ===== - protected suspend fun newClient(serverUrl: String): Client = - HttpClient(CIO) { install(SSE) }.mcpStreamableHttp(serverUrl) - - protected suspend fun withClient(serverUrl: String, block: suspend (Client) -> T): T { - val client = newClient(serverUrl) - return try { - withTimeout(20.seconds) { block(client) } - } finally { - try { - withTimeout(3.seconds) { client.close() } - } catch (_: Exception) { - // ignore errors - } - } - } - - // ===== STDIO client + server helpers ===== - protected fun startTypeScriptServerStdio(): Process { - if (!sdkDir.exists() || !sdkDir.isDirectory) { - throw IllegalStateException( - "TypeScript SDK directory does not exist or is not accessible: ${sdkDir.absolutePath}", - ) - } - val isWindows = System.getProperty("os.name").lowercase().contains("windows") - val localServerPath = File(tsClientDir, "simpleStdio.ts").absolutePath - val processBuilder = if (isWindows) { - ProcessBuilder() - .command( - "cmd.exe", - "/c", - "set NODE_PATH=${sdkDir.absolutePath}\\node_modules && npx --prefix \"${sdkDir.absolutePath}\" tsx \"$localServerPath\"", - ) - } else { - ProcessBuilder() - .command( - "bash", - "-c", - "NODE_PATH='${sdkDir.absolutePath}/node_modules' npx --prefix '${sdkDir.absolutePath}' tsx \"$localServerPath\"", - ) - } - processBuilder.environment()["TYPESCRIPT_SDK_DIR"] = sdkDir.absolutePath - val process = processBuilder - .directory(tsClientDir) - .redirectErrorStream(false) - .start() - // For stdio transports, do NOT read from stdout (it's used for protocol). Read stderr for logs only. - createProcessErrorReader(process, prefix = "TS-SERVER-STDIO").start() - // Give the process a moment to start - await.atMost(2, TimeUnit.SECONDS) - .pollDelay(200, TimeUnit.MILLISECONDS) - .pollInterval(100, TimeUnit.MILLISECONDS) - .until { process.isAlive } - return process - } - - protected suspend fun newClientStdio(process: Process): Client { - val input: Source = process.inputStream.asSource().buffered() - val output: Sink = process.outputStream.asSink().buffered() - val transport = StdioClientTransport(input = input, output = output) - val client = Client(Implementation("test", "1.0")) - client.connect(transport) - return client - } - - protected suspend fun withClientStdio(block: suspend (Client, Process) -> T): T { - val proc = startTypeScriptServerStdio() - val client = newClientStdio(proc) - return try { - withTimeout(20.seconds) { block(client, proc) } - } finally { - try { - withTimeout(3.seconds) { client.close() } - } catch (_: Exception) { - } - try { - stopProcess(proc, name = "TypeScript stdio server") - } catch (_: Exception) { - } - } - } - - // ===== Helpers to run TypeScript client over STDIO against Kotlin server over STDIO ===== - protected fun runStdioClient(vararg args: String): String { - // Start Node stdio client (it will speak MCP over its stdout/stdin) - val isWindows = System.getProperty("os.name").lowercase().contains("windows") - val clientPath = File(tsClientDir, "myClient.ts").absolutePath - - val process = if (isWindows) { - ProcessBuilder() - .command( - "cmd.exe", - "/c", - ( - "set TYPESCRIPT_SDK_DIR=${sdkDir.absolutePath} && " + - "set NODE_PATH=${sdkDir.absolutePath}\\node_modules && " + - "npx --prefix \"${sdkDir.absolutePath}\" tsx \"$clientPath\" " + - args.joinToString(" ") - ), - ) - .directory(tsClientDir) - .redirectErrorStream(false) - .start() - } else { - ProcessBuilder() - .command( - "bash", - "-c", - ( - "TYPESCRIPT_SDK_DIR='${sdkDir.absolutePath}' " + - "NODE_PATH='${sdkDir.absolutePath}/node_modules' " + - "npx --prefix '${sdkDir.absolutePath}' tsx \"$clientPath\" " + - args.joinToString(" ") - ), - ) - .directory(tsClientDir) - .redirectErrorStream(false) - .start() - } - - // Create Kotlin server and attach stdio transport to the process streams - val server: Server = KotlinServerForTsClient().createMcpServer() - val transport = StdioServerTransport( - inputStream = process.inputStream.asSource().buffered(), - outputStream = process.outputStream.asSink().buffered(), - ) - - // Connect server in a background thread to avoid blocking - val serverThread = Thread { - try { - kotlinx.coroutines.runBlocking { server.connect(transport) } - } catch (e: Exception) { - println("[STDIO-SERVER] Error connecting: ${e.message}") - } - } - serverThread.isDaemon = true - serverThread.start() - - // Read ONLY stderr from client for human-readable output - val output = StringBuilder() - val errReader = Thread { - try { - process.errorStream.bufferedReader().useLines { lines -> - lines.forEach { line -> - println("[TS-CLIENT-STDIO][err] $line") - output.append(line).append('\n') - } - } - } catch (e: Exception) { - println("Warning: Error reading stdio client stderr: ${e.message}") - } - } - errReader.isDaemon = true - errReader.start() - - // Wait up to 25s for client to exit - val finished = process.waitFor(25, TimeUnit.SECONDS) - if (!finished) { - println("Stdio client did not finish in time; destroying") - process.destroyForcibly() - } - - try { - kotlinx.coroutines.runBlocking { transport.close() } - } catch (_: Exception) { - } - - return output.toString() - } -} diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerEdgeCasesTestSse.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerEdgeCasesTestSse.kt deleted file mode 100644 index 1585aa5..0000000 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerEdgeCasesTestSse.kt +++ /dev/null @@ -1,225 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse - -import io.modelcontextprotocol.kotlin.sdk.CallToolResult -import io.modelcontextprotocol.kotlin.sdk.TextContent -import io.modelcontextprotocol.kotlin.sdk.client.Client -import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind -import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TsTestBase -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withTimeout -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.Timeout -import org.junit.jupiter.api.assertThrows -import java.util.concurrent.TimeUnit -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertTrue -import kotlin.time.Duration.Companion.seconds - -class KotlinClientTsServerEdgeCasesTestSse : TsTestBase() { - - override val transportKind = TransportKind.SSE - - private var port: Int = 0 - private val host = "localhost" - private lateinit var serverUrl: String - - private lateinit var client: Client - private lateinit var tsServerProcess: Process - - @BeforeEach - fun setUp() { - port = findFreePort() - serverUrl = "http://$host:$port/mcp" - tsServerProcess = startTypeScriptServer(port) - println("TypeScript server started on port $port") - } - - @AfterEach - fun tearDown() { - if (::client.isInitialized) { - try { - runBlocking { - withTimeout(3.seconds) { - client.close() - } - } - } catch (e: Exception) { - println("Warning: Error during client close: ${e.message}") - } - } - - if (::tsServerProcess.isInitialized) { - try { - println("Stopping TypeScript server") - stopProcess(tsServerProcess) - } catch (e: Exception) { - println("Warning: Error during TypeScript server stop: ${e.message}") - } - } - } - - @Test - @Timeout(30, unit = TimeUnit.SECONDS) - fun testNonExistentTool(): Unit = runBlocking(Dispatchers.IO) { - withClient(serverUrl) { client -> - val nonExistentToolName = "non-existent-tool" - val arguments = mapOf("name" to "TestUser") - - val exception = assertThrows { - client.callTool(nonExistentToolName, arguments) - } - - val expectedMessage = - "JSONRPCError(code=InvalidParams, message=MCP error -32602: Tool non-existent-tool not found, data={})" - assertEquals( - expectedMessage, - exception.message, - "Unexpected error message for non-existent tool", - ) - } - } - - @Test - @Timeout(30, unit = TimeUnit.SECONDS) - fun testSpecialCharactersInArguments(): Unit = runBlocking(Dispatchers.IO) { - withClient(serverUrl) { client -> - val specialChars = "!@#$%^&*()_+{}[]|\\:;\"'<>,.?/" - val arguments = mapOf("name" to specialChars) - - val result = client.callTool("greet", arguments) - assertNotNull(result, "Tool call result should not be null") - - val callResult = result as CallToolResult - val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent - assertNotNull(textContent, "Text content should be present in the result") - - val text = textContent.text ?: "" - assertTrue( - text.contains(specialChars), - "Tool response should contain the special characters", - ) - } - } - - @Test - @Timeout(30, unit = TimeUnit.SECONDS) - fun testLargePayload(): Unit = runBlocking(Dispatchers.IO) { - withClient(serverUrl) { client -> - val largeName = "A".repeat(10 * 1024) - val arguments = mapOf("name" to largeName) - - val result = client.callTool("greet", arguments) - assertNotNull(result, "Tool call result should not be null") - - val callResult = result as CallToolResult - val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent - assertNotNull(textContent, "Text content should be present in the result") - - val text = textContent.text ?: "" - assertTrue( - text.contains("Hello,") && text.contains("A"), - "Tool response should contain the greeting with the large name", - ) - } - } - - @Test - @Timeout(60, unit = TimeUnit.SECONDS) - fun testConcurrentRequests(): Unit = runBlocking(Dispatchers.IO) { - withClient(serverUrl) { client -> - val concurrentCount = 5 - val responses = coroutineScope { - val results = (1..concurrentCount).map { i -> - async { - val name = "ConcurrentClient$i" - val arguments = mapOf("name" to name) - - val result = client.callTool("greet", arguments) - assertNotNull(result, "Tool call result should not be null for client $i") - - val callResult = result as CallToolResult - val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent - assertNotNull(textContent, "Text content should be present for client $i") - - textContent.text ?: "" - } - } - results.awaitAll() - } - - for (i in 1..concurrentCount) { - val expectedName = "ConcurrentClient$i" - val matchingResponses = responses.filter { it.contains("Hello, $expectedName!") } - assertEquals( - 1, - matchingResponses.size, - "Should have exactly one response for $expectedName", - ) - } - } - } - - @Test - @Timeout(30, unit = TimeUnit.SECONDS) - fun testInvalidArguments(): Unit = runBlocking(Dispatchers.IO) { - withClient(serverUrl) { client -> - val invalidArguments = mapOf( - "name" to JsonObject(mapOf("nested" to JsonPrimitive("value"))), - ) - - val exception = assertThrows { - client.callTool("greet", invalidArguments) - } - - val msg = exception.message ?: "" - val expectedMessage = """ - JSONRPCError(code=InvalidParams, message=MCP error -32602: Invalid arguments for tool greet: [ - { - "code": "invalid_type", - "expected": "string", - "received": "object", - "path": [ - "name" - ], - "message": "Expected string, received object" - } - ], data={}) - """.trimIndent() - - assertEquals(expectedMessage, msg, "Unexpected error message for invalid arguments") - } - } - - @Test - @Timeout(30, unit = TimeUnit.SECONDS) - fun testMultipleToolCalls(): Unit = runBlocking(Dispatchers.IO) { - withClient(serverUrl) { client -> - repeat(10) { i -> - val name = "SequentialClient$i" - val arguments = mapOf("name" to name) - - val result = client.callTool("greet", arguments) - assertNotNull(result, "Tool call result should not be null for call $i") - - val callResult = result as CallToolResult - val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent - assertNotNull(textContent, "Text content should be present for call $i") - - assertEquals( - "Hello, $name!", - textContent.text, - "Tool response should contain the greeting with the provided name", - ) - } - } - } -} diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerTestSse.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerTestSse.kt deleted file mode 100644 index 95a80f0..0000000 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerTestSse.kt +++ /dev/null @@ -1,49 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse - -import io.modelcontextprotocol.kotlin.sdk.client.Client -import io.modelcontextprotocol.kotlin.sdk.integration.typescript.AbstractKotlinClientTsServerTest -import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind -import kotlinx.coroutines.withTimeout -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import kotlin.time.Duration.Companion.seconds - -class KotlinClientTsServerTestSse : AbstractKotlinClientTsServerTest() { - - override val transportKind = TransportKind.SSE - - private var port: Int = 0 - private val host = "localhost" - private lateinit var serverUrl: String - private lateinit var tsServerProcess: Process - - @BeforeEach - fun setUpSse() { - port = findFreePort() - serverUrl = "http://$host:$port/mcp" - tsServerProcess = startTypeScriptServer(port) - println("TypeScript server started on port $port") - } - - @AfterEach - fun tearDownSse() { - if (::tsServerProcess.isInitialized) { - try { - println("Stopping TypeScript server") - stopProcess(tsServerProcess) - } catch (e: Exception) { - println("Warning: Error during TypeScript server stop: ${e.message}") - } - } - } - - override suspend fun useClient(block: suspend (Client) -> T): T = withClient(serverUrl) { client -> - try { - withTimeout(20.seconds) { block(client) } - } finally { - try { - withTimeout(3.seconds) { client.close() } - } catch (_: Exception) {} - } - } -} diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/myClient.ts b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/myClient.ts deleted file mode 100644 index 42a14f5..0000000 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/myClient.ts +++ /dev/null @@ -1,129 +0,0 @@ -// @ts-ignore -const args = process.argv.slice(2); -const serverUrl = args[0] || 'http://localhost:3001/mcp'; -const toolName = args[1]; -const toolArgs = args.slice(2); -const PROTOCOL_VERSION = "2024-11-05"; - -// @ts-ignore -async function main() { - // @ts-ignore - const sdkDirRaw = process.env.TYPESCRIPT_SDK_DIR; - const sdkDir = sdkDirRaw ? sdkDirRaw.trim() : undefined; - let Client: any; - let StreamableHTTPClientTransport: any; - if (sdkDir) { - // @ts-ignore - const path = await import('path'); - // @ts-ignore - const { pathToFileURL } = await import('url'); - const clientUrl = pathToFileURL(path.join(sdkDir, 'src', 'client', 'index.ts')).href; - const streamUrl = pathToFileURL(path.join(sdkDir, 'src', 'client', 'streamableHttp.js')).href; - // @ts-ignore - ({ Client } = await import(clientUrl)); - // @ts-ignore - ({ StreamableHTTPClientTransport } = await import(streamUrl)); - } else { - // @ts-ignore - ({Client} = await import("../../../../../../../resources/typescript-sdk/src/client")); - // @ts-ignore - ({StreamableHTTPClientTransport} = await import("../../../../../../../resources/typescript-sdk/src/client/streamableHttp.js")); - } - if (!toolName) { - console.log('Usage: npx tsx myClient.ts [server-url] [tool-args...]'); - console.log('Using default server URL:', serverUrl); - console.log('Available utils will be listed after connection'); - } - - console.log(`Connecting to server at ${serverUrl}`); - if (toolName) { - console.log(`Will call tool: ${toolName} with args: ${toolArgs.join(', ')}`); - } - - const client = new Client({ - name: 'test-client', - version: '1.0.0' - }); - - const transport = new StreamableHTTPClientTransport(new URL(serverUrl)); - - try { - await client.connect(transport, {protocolVersion: PROTOCOL_VERSION}); - console.log('Connected to server'); - - try { - if (typeof (client as any).on === 'function') { - (client as any).on('notification', (n: any) => { - try { - const method = (n && (n.method || (n.params && n.params.method))) || 'unknown'; - console.log('Notification:', method, JSON.stringify(n)); - } catch { - console.log('Notification: '); - } - }); - } - } catch { - } - - const toolsResult = await client.listTools(); - const tools = toolsResult.tools; - console.log('Available utils:', tools.map((t: { name: any; }) => t.name).join(', ')); - - if (!toolName) { - await client.close(); - return; - } - - const tool = tools.find((t: { name: string; }) => t.name === toolName); - if (!tool) { - console.error(`Tool "${toolName}" not found`); - // @ts-ignore - process.exit(1); - } - - const toolArguments = {}; - - if (toolName === "greet" && toolArgs.length > 0) { - toolArguments["name"] = toolArgs[0]; - } else if (tool.input && tool.input.properties) { - const propNames = Object.keys(tool.input.properties); - if (propNames.length > 0 && toolArgs.length > 0) { - toolArguments[propNames[0]] = toolArgs[0]; - } - } - - console.log(`Calling tool ${toolName} with arguments:`, toolArguments); - - const result = await client.callTool({ - name: toolName, - arguments: toolArguments - }); - console.log('Tool result:', result); - - if (result.content) { - for (const content of result.content) { - if (content.type === 'text') { - console.log('Text content:', content.text); - } - } - } - - if (result.structuredContent) { - console.log('Structured content:', JSON.stringify(result.structuredContent, null, 2)); - } - - } catch (error) { - console.error('Error:', error); - // @ts-ignore - process.exit(1); - } finally { - await client.close(); - console.log('Disconnected from server'); - } -} - -main().catch(error => { - console.error('Unhandled error:', error); - // @ts-ignore - process.exit(1); -}); diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/simpleStreamableHttp.ts b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/simpleStreamableHttp.ts deleted file mode 100644 index c750aed..0000000 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/simpleStreamableHttp.ts +++ /dev/null @@ -1,701 +0,0 @@ -// @ts-nocheck -import express, {Request, Response} from 'express'; -import {randomUUID} from 'node:crypto'; -import {z} from 'zod'; -import cors from 'cors'; -import path from 'node:path'; -import {pathToFileURL} from 'node:url'; - -const SDK_DIR = process.env.TYPESCRIPT_SDK_DIR; -if (!SDK_DIR) { - throw new Error('TYPESCRIPT_SDK_DIR environment variable is not set. It should point to the cloned typescript-sdk directory.'); -} - -async function importFromSdk(rel: string): Promise { - const full = path.resolve(SDK_DIR!, rel); - const url = pathToFileURL(full).href; - return (await import(url)) as unknown as T; -} - -async function main() { - const {McpServer} = await importFromSdk('src/server/mcp.ts'); - const {StreamableHTTPServerTransport} = await importFromSdk('src/server/streamableHttp.ts'); - const { - getOAuthProtectedResourceMetadataUrl, - mcpAuthMetadataRouter - } = await importFromSdk('src/server/auth/router.ts'); - const {requireBearerAuth} = await importFromSdk('src/server/auth/middleware/bearerAuth.ts'); - const { - isInitializeRequest, - } = await importFromSdk('src/types.ts'); - const {InMemoryEventStore} = await importFromSdk('src/examples/shared/inMemoryEventStore.ts'); - const {setupAuthServer} = await importFromSdk('src/examples/server/demoInMemoryOAuthProvider.ts'); - const {checkResourceAllowed} = await importFromSdk('src/shared/auth-utils.ts'); - - // Check for OAuth flag - const useOAuth = process.argv.includes('--oauth'); - const strictOAuth = process.argv.includes('--oauth-strict'); - -// Create an MCP server with implementation details - const getServer = () => { - const server = new McpServer({ - name: 'simple-streamable-http-server', - version: '1.0.0' - }, {capabilities: {logging: {}}}); - - // Register a simple tool that returns a greeting - server.registerTool( - 'greet', - { - title: 'Greeting Tool', // Display name for UI - description: 'A simple greeting tool', - inputSchema: { - name: z.string().describe('Name to greet'), - }, - }, - async ({name}): Promise => { - return { - content: [ - { - type: 'text', - text: `Hello, ${name}!`, - }, - ], - }; - } - ); - - // Register a tool that sends multiple greetings with notifications (with annotations) - server.tool( - 'multi-greet', - 'A tool that sends different greetings with delays between them', - { - name: z.string().describe('Name to greet'), - }, - { - title: 'Multiple Greeting Tool', - readOnlyHint: true, - openWorldHint: false - }, - async ({name}, extra): Promise => { - const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); - - await server.sendLoggingMessage({ - level: "debug", - data: `Starting multi-greet for ${name}` - }, extra.sessionId); - - await sleep(1000); // Wait 1 second before first greeting - - await server.sendLoggingMessage({ - level: "info", - data: `Sending first greeting to ${name}` - }, extra.sessionId); - - await sleep(1000); // Wait another second before second greeting - - await server.sendLoggingMessage({ - level: "info", - data: `Sending second greeting to ${name}` - }, extra.sessionId); - - return { - content: [ - { - type: 'text', - text: `Good morning, ${name}!`, - } - ], - }; - } - ); - // Register a tool that demonstrates elicitation (user input collection) - // This creates a closure that captures the server instance - server.tool( - 'collect-user-info', - 'A tool that collects user information through elicitation', - { - infoType: z.enum(['contact', 'preferences', 'feedback']).describe('Type of information to collect'), - }, - async ({infoType}): Promise => { - let message: string; - let requestedSchema: { - type: 'object'; - properties: Record; - required?: string[]; - }; - - switch (infoType) { - case 'contact': - message = 'Please provide your contact information'; - requestedSchema = { - type: 'object', - properties: { - name: { - type: 'string', - title: 'Full Name', - description: 'Your full name', - }, - email: { - type: 'string', - title: 'Email Address', - description: 'Your email address', - format: 'email', - }, - phone: { - type: 'string', - title: 'Phone Number', - description: 'Your phone number (optional)', - }, - }, - required: ['name', 'email'], - }; - break; - case 'preferences': - message = 'Please set your preferences'; - requestedSchema = { - type: 'object', - properties: { - theme: { - type: 'string', - title: 'Theme', - description: 'Choose your preferred theme', - enum: ['light', 'dark', 'auto'], - enumNames: ['Light', 'Dark', 'Auto'], - }, - notifications: { - type: 'boolean', - title: 'Enable Notifications', - description: 'Would you like to receive notifications?', - default: true, - }, - frequency: { - type: 'string', - title: 'Notification Frequency', - description: 'How often would you like notifications?', - enum: ['daily', 'weekly', 'monthly'], - enumNames: ['Daily', 'Weekly', 'Monthly'], - }, - }, - required: ['theme'], - }; - break; - case 'feedback': - message = 'Please provide your feedback'; - requestedSchema = { - type: 'object', - properties: { - rating: { - type: 'integer', - title: 'Rating', - description: 'Rate your experience (1-5)', - minimum: 1, - maximum: 5, - }, - comments: { - type: 'string', - title: 'Comments', - description: 'Additional comments (optional)', - maxLength: 500, - }, - recommend: { - type: 'boolean', - title: 'Would you recommend this?', - description: 'Would you recommend this to others?', - }, - }, - required: ['rating', 'recommend'], - }; - break; - default: - throw new Error(`Unknown info type: ${infoType}`); - } - - try { - // Use the underlying server instance to elicit input from the client - const result = await server.server.elicitInput({ - message, - requestedSchema, - }); - - if (result.action === 'accept') { - return { - content: [ - { - type: 'text', - text: `Thank you! Collected ${infoType} information: ${JSON.stringify(result.content, null, 2)}`, - }, - ], - }; - } else if (result.action === 'decline') { - return { - content: [ - { - type: 'text', - text: `No information was collected. User declined ${infoType} information request.`, - }, - ], - }; - } else { - return { - content: [ - { - type: 'text', - text: `Information collection was cancelled by the user.`, - }, - ], - }; - } - } catch (error) { - return { - content: [ - { - type: 'text', - text: `Error collecting ${infoType} information: ${error}`, - }, - ], - }; - } - } - ); - - // Register a simple prompt with title - server.registerPrompt( - 'greeting-template', - { - title: 'Greeting Template', // Display name for UI - description: 'A simple greeting prompt template', - argsSchema: { - name: z.string().describe('Name to include in greeting'), - }, - }, - async ({name}): Promise => { - return { - messages: [ - { - role: 'user', - content: { - type: 'text', - text: `Please greet ${name} in a friendly manner.`, - }, - }, - ], - }; - } - ); - - // Register a tool specifically for testing resumability - server.tool( - 'start-notification-stream', - 'Starts sending periodic notifications for testing resumability', - { - interval: z.number().describe('Interval in milliseconds between notifications').default(100), - count: z.number().describe('Number of notifications to send (0 for 100)').default(50), - }, - async ({interval, count}, extra): Promise => { - const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); - let counter = 0; - - while (count === 0 || counter < count) { - counter++; - try { - await server.sendLoggingMessage({ - level: "info", - data: `Periodic notification #${counter} at ${new Date().toISOString()}` - }, extra.sessionId); - } catch (error) { - console.error("Error sending notification:", error); - } - // Wait for the specified interval - await sleep(interval); - } - - return { - content: [ - { - type: 'text', - text: `Started sending periodic notifications every ${interval}ms`, - } - ], - }; - } - ); - - // Create a simple resource at a fixed URI - server.registerResource( - 'greeting-resource', - 'https://example.com/greetings/default', - { - title: 'Default Greeting', // Display name for UI - description: 'A simple greeting resource', - mimeType: 'text/plain' - }, - async (): Promise => { - return { - contents: [ - { - uri: 'https://example.com/greetings/default', - text: 'Hello, world!', - }, - ], - }; - } - ); - - // Create additional resources for ResourceLink demonstration - server.registerResource( - 'example-file-1', - 'file:///example/file1.txt', - { - title: 'Example File 1', - description: 'First example file for ResourceLink demonstration', - mimeType: 'text/plain' - }, - async (): Promise => { - return { - contents: [ - { - uri: 'file:///example/file1.txt', - text: 'This is the content of file 1', - }, - ], - }; - } - ); - - server.registerResource( - 'example-file-2', - 'file:///example/file2.txt', - { - title: 'Example File 2', - description: 'Second example file for ResourceLink demonstration', - mimeType: 'text/plain' - }, - async (): Promise => { - return { - contents: [ - { - uri: 'file:///example/file2.txt', - text: 'This is the content of file 2', - }, - ], - }; - } - ); - - // Register a tool that returns ResourceLinks - server.registerTool( - 'list-files', - { - title: 'List Files with ResourceLinks', - description: 'Returns a list of files as ResourceLinks without embedding their content', - inputSchema: { - includeDescriptions: z.boolean().optional().describe('Whether to include descriptions in the resource links'), - }, - }, - async ({includeDescriptions = true}): Promise => { - const resourceLinks: ResourceLink[] = [ - { - type: 'resource_link', - uri: 'https://example.com/greetings/default', - name: 'Default Greeting', - mimeType: 'text/plain', - ...(includeDescriptions && {description: 'A simple greeting resource'}) - }, - { - type: 'resource_link', - uri: 'file:///example/file1.txt', - name: 'Example File 1', - mimeType: 'text/plain', - ...(includeDescriptions && {description: 'First example file for ResourceLink demonstration'}) - }, - { - type: 'resource_link', - uri: 'file:///example/file2.txt', - name: 'Example File 2', - mimeType: 'text/plain', - ...(includeDescriptions && {description: 'Second example file for ResourceLink demonstration'}) - } - ]; - - return { - content: [ - { - type: 'text', - text: 'Here are the available files as resource links:', - }, - ...resourceLinks, - { - type: 'text', - text: '\nYou can read any of these resources using their URI.', - } - ], - }; - } - ); - - return server; - }; - - const MCP_PORT = process.env.MCP_PORT ? parseInt(process.env.MCP_PORT, 10) : 3000; - const AUTH_PORT = process.env.MCP_AUTH_PORT ? parseInt(process.env.MCP_AUTH_PORT, 10) : 3001; - - const app = express(); - app.use(express.json()); - -// Allow CORS all domains, expose the Mcp-Session-Id header - app.use(cors({ - origin: '*', // Allow all origins - exposedHeaders: ["Mcp-Session-Id"] - })); - -// Set up OAuth if enabled - let authMiddleware = null; - if (useOAuth) { - // Create auth middleware for MCP endpoints - const mcpServerUrl = new URL(`http://localhost:${MCP_PORT}/mcp`); - const authServerUrl = new URL(`http://localhost:${AUTH_PORT}`); - - const oauthMetadata: OAuthMetadata = setupAuthServer({ - authServerUrl, - mcpServerUrl, - strictResource: strictOAuth - }); - - const tokenVerifier = { - verifyAccessToken: async (token: string) => { - const endpoint = oauthMetadata.introspection_endpoint; - - if (!endpoint) { - throw new Error('No token verification endpoint available in metadata'); - } - - const response = await fetch(endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams({ - token: token - }).toString() - }); - - - if (!response.ok) { - throw new Error(`Invalid or expired token: ${await response.text()}`); - } - - const data = await response.json(); - - if (strictOAuth) { - if (!data.aud) { - throw new Error(`Resource Indicator (RFC8707) missing`); - } - if (!checkResourceAllowed({requestedResource: data.aud, configuredResource: mcpServerUrl})) { - throw new Error(`Expected resource indicator ${mcpServerUrl}, got: ${data.aud}`); - } - } - - // Convert the response to AuthInfo format - return { - token, - clientId: data.client_id, - scopes: data.scope ? data.scope.split(' ') : [], - expiresAt: data.exp, - }; - } - } - // Add metadata routes to the main MCP server - app.use(mcpAuthMetadataRouter({ - oauthMetadata, - resourceServerUrl: mcpServerUrl, - scopesSupported: ['mcp:tools'], - resourceName: 'MCP Demo Server', - })); - - authMiddleware = requireBearerAuth({ - verifier: tokenVerifier, - requiredScopes: [], - resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl), - }); - } - -// Map to store transports by session ID - const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; - -// MCP POST endpoint with optional auth - const mcpPostHandler = async (req: Request, res: Response) => { - const sessionId = req.headers['mcp-session-id'] as string | undefined; - if (sessionId) { - console.log(`Received MCP request for session: ${sessionId}`); - } else { - console.log('Request body:', req.body); - } - - if (useOAuth && req.auth) { - console.log('Authenticated user:', req.auth); - } - try { - let transport: StreamableHTTPServerTransport; - if (sessionId && transports[sessionId]) { - // Reuse existing transport - transport = transports[sessionId]; - } else if (!sessionId && isInitializeRequest(req.body)) { - // New initialization request - const eventStore = new InMemoryEventStore(); - transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: () => randomUUID(), - eventStore, // Enable resumability - onsessioninitialized: (sessionId) => { - // Store the transport by session ID when session is initialized - // This avoids race conditions where requests might come in before the session is stored - console.log(`Session initialized with ID: ${sessionId}`); - transports[sessionId] = transport; - } - }); - - // Set up onclose handler to clean up transport when closed - transport.onclose = () => { - const sid = transport.sessionId; - if (sid && transports[sid]) { - console.log(`Transport closed for session ${sid}, removing from transports map`); - delete transports[sid]; - } - }; - - // Connect the transport to the MCP server BEFORE handling the request - // so responses can flow back through the same transport - const server = getServer(); - await server.connect(transport); - - await transport.handleRequest(req, res, req.body); - return; // Already handled - } else { - // Invalid request - no session ID or not initialization request - res.status(400).json({ - jsonrpc: '2.0', - error: { - code: -32000, - message: 'Bad Request: No valid session ID provided', - }, - id: null, - }); - return; - } - - // Handle the request with existing transport - no need to reconnect - // The existing transport is already connected to the server - await transport.handleRequest(req, res, req.body); - } catch (error) { - console.error('Error handling MCP request:', error); - if (!res.headersSent) { - res.status(500).json({ - jsonrpc: '2.0', - error: { - code: -32603, - message: 'Internal server error', - }, - id: null, - }); - } - } - }; - -// Set up routes with conditional auth middleware - if (useOAuth && authMiddleware) { - app.post('/mcp', authMiddleware, mcpPostHandler); - } else { - app.post('/mcp', mcpPostHandler); - } - -// Handle GET requests for SSE streams (using built-in support from StreamableHTTP) - const mcpGetHandler = async (req: Request, res: Response) => { - const sessionId = req.headers['mcp-session-id'] as string | undefined; - if (!sessionId || !transports[sessionId]) { - res.status(400).send('Invalid or missing session ID'); - return; - } - - if (useOAuth && req.auth) { - console.log('Authenticated SSE connection from user:', req.auth); - } - - // Check for Last-Event-ID header for resumability - const lastEventId = req.headers['last-event-id'] as string | undefined; - if (lastEventId) { - console.log(`Client reconnecting with Last-Event-ID: ${lastEventId}`); - } else { - console.log(`Establishing new SSE stream for session ${sessionId}`); - } - - const transport = transports[sessionId]; - await transport.handleRequest(req, res); - }; - -// Set up GET route with conditional auth middleware - if (useOAuth && authMiddleware) { - app.get('/mcp', authMiddleware, mcpGetHandler); - } else { - app.get('/mcp', mcpGetHandler); - } - -// Handle DELETE requests for session termination (according to MCP spec) - const mcpDeleteHandler = async (req: Request, res: Response) => { - const sessionId = req.headers['mcp-session-id'] as string | undefined; - if (!sessionId || !transports[sessionId]) { - res.status(400).send('Invalid or missing session ID'); - return; - } - - console.log(`Received session termination request for session ${sessionId}`); - - try { - const transport = transports[sessionId]; - await transport.handleRequest(req, res); - } catch (error) { - console.error('Error handling session termination:', error); - if (!res.headersSent) { - res.status(500).send('Error processing session termination'); - } - } - }; - -// Set up DELETE route with conditional auth middleware - if (useOAuth && authMiddleware) { - app.delete('/mcp', authMiddleware, mcpDeleteHandler); - } else { - app.delete('/mcp', mcpDeleteHandler); - } - - app.listen(MCP_PORT, (error) => { - if (error) { - console.error('Failed to start server:', error); - process.exit(1); - } - console.log(`MCP Streamable HTTP Server listening on port ${MCP_PORT}`); - }); - -// Handle server shutdown - process.on('SIGINT', async () => { - console.log('Shutting down server...'); - - // Close all active transports to properly clean up resources - for (const sessionId in transports) { - try { - console.log(`Closing transport for session ${sessionId}`); - await transports[sessionId].close(); - delete transports[sessionId]; - } catch (error) { - console.error(`Error closing transport for session ${sessionId}:`, error); - } - } - console.log('Server shutdown complete'); - process.exit(0); - }); - -} - -main().catch((err) => { - console.error('Failed to start server:', err); - process.exit(1); -}); diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/simpleStdio.ts b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/simpleStdio.ts deleted file mode 100644 index 2986309..0000000 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/simpleStdio.ts +++ /dev/null @@ -1,74 +0,0 @@ -// @ts-nocheck -import { z } from 'zod'; -import path from 'node:path'; -import { pathToFileURL } from 'node:url'; - -const SDK_DIR = process.env.TYPESCRIPT_SDK_DIR; -if (!SDK_DIR) { - throw new Error('TYPESCRIPT_SDK_DIR environment variable is not set. It should point to the cloned typescript-sdk directory.'); -} - -async function importFromSdk(rel: string): Promise { - const full = path.resolve(SDK_DIR!, rel); - const url = pathToFileURL(full).href; - return await import(url); -} - -async function main() { - const { McpServer } = await importFromSdk('src/server/mcp.ts'); - const { StdioServerTransport } = await importFromSdk('src/server/stdio.ts'); - - const server = new McpServer({ - name: 'simple-stdio-server', - version: '1.0.0', - }, { capabilities: { logging: {} } }); - - // Simple tools mirroring ones from HTTP test server - server.registerTool('greet', { - title: 'Greeting Tool', - description: 'A simple greeting tool', - inputSchema: { name: z.string().describe('Name to greet') }, - }, async ({ name }): Promise => { - return { content: [{ type: 'text', text: `Hello, ${name}!` }] }; - }); - - server.tool('multi-greet', 'A tool that sends different greetings with delays between them', - { name: z.string().describe('Name to greet') }, - { title: 'Multiple Greeting Tool', readOnlyHint: true, openWorldHint: false }, - async ({ name }, extra): Promise => { - const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)); - await server.sendLoggingMessage({ level: 'debug', data: `Starting multi-greet for ${name}` }, extra.sessionId); - await sleep(200); - await server.sendLoggingMessage({ level: 'info', data: `Sending first greeting to ${name}` }, extra.sessionId); - await sleep(200); - await server.sendLoggingMessage({ level: 'info', data: `Sending second greeting to ${name}` }, extra.sessionId); - return { content: [{ type: 'text', text: `Good morning, ${name}!` }] }; - } - ); - - server.registerPrompt('greeting-template', { - title: 'Greeting Template', - description: 'A simple greeting prompt template', - argsSchema: { name: z.string().describe('Name to include in greeting') }, - }, async ({ name }): Promise => { - return { - messages: [{ role: 'user', content: { type: 'text', text: `Please greet ${name} in a friendly manner.` } }], - }; - }); - - server.registerResource('greeting-resource', 'https://example.com/greetings/default', { - title: 'Default Greeting', - description: 'A simple greeting resource', - mimeType: 'text/plain', - }, async (): Promise => { - return { contents: [{ uri: 'https://example.com/greetings/default', text: 'Hello, world!' }] }; - }); - - const transport = new StdioServerTransport(); - await server.connect(transport); -} - -main().catch((err) => { - console.error('Failed to start stdio server:', err); - process.exit(1); -}); diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerTest.kt deleted file mode 100644 index 77175b4..0000000 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerTest.kt +++ /dev/null @@ -1,474 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk.server - -import io.modelcontextprotocol.kotlin.sdk.CallToolResult -import io.modelcontextprotocol.kotlin.sdk.GetPromptResult -import io.modelcontextprotocol.kotlin.sdk.Implementation -import io.modelcontextprotocol.kotlin.sdk.Method -import io.modelcontextprotocol.kotlin.sdk.Prompt -import io.modelcontextprotocol.kotlin.sdk.PromptListChangedNotification -import io.modelcontextprotocol.kotlin.sdk.ReadResourceResult -import io.modelcontextprotocol.kotlin.sdk.ResourceListChangedNotification -import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities -import io.modelcontextprotocol.kotlin.sdk.TextContent -import io.modelcontextprotocol.kotlin.sdk.TextResourceContents -import io.modelcontextprotocol.kotlin.sdk.Tool -import io.modelcontextprotocol.kotlin.sdk.ToolListChangedNotification -import io.modelcontextprotocol.kotlin.sdk.client.Client -import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -class ServerTest { - - @Test - fun `removeTool should remove a tool`() = runTest { - // Create server with tools capability - val serverOptions = ServerOptions( - capabilities = ServerCapabilities( - tools = ServerCapabilities.Tools(null), - ), - ) - val server = Server( - Implementation(name = "test server", version = "1.0"), - serverOptions, - ) - - // Add a tool - server.addTool("test-tool", "Test Tool", Tool.Input()) { - CallToolResult(listOf(TextContent("Test result"))) - } - - // Setup client - val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() - val client = Client( - clientInfo = Implementation(name = "test client", version = "1.0"), - ) - - // Connect client and server - launch { client.connect(clientTransport) } - launch { server.connect(serverTransport) } - - // Remove the tool - val result = server.removeTool("test-tool") - - // Verify the tool was removed - assertTrue(result, "Tool should be removed successfully") - } - - @Test - fun `removeTool should return false when tool does not exist`() = runTest { - // Create server with tools capability - val serverOptions = ServerOptions( - capabilities = ServerCapabilities( - tools = ServerCapabilities.Tools(null), - ), - ) - val server = Server( - Implementation(name = "test server", version = "1.0"), - serverOptions, - ) - - // Setup client - val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() - val client = Client( - clientInfo = Implementation(name = "test client", version = "1.0"), - ) - - // Track notifications - var toolListChangedNotificationReceived = false - client.setNotificationHandler(Method.Defined.NotificationsToolsListChanged) { - toolListChangedNotificationReceived = true - CompletableDeferred(Unit) - } - - // Connect client and server - launch { client.connect(clientTransport) } - launch { server.connect(serverTransport) } - - // Try to remove a non-existent tool - val result = server.removeTool("non-existent-tool") - - // Verify the result - assertFalse(result, "Removing non-existent tool should return false") - assertFalse(toolListChangedNotificationReceived, "No notification should be sent when tool doesn't exist") - } - - @Test - fun `removeTool should throw when tools capability is not supported`() = runTest { - // Create server without tools capability - val serverOptions = ServerOptions( - capabilities = ServerCapabilities(), - ) - val server = Server( - Implementation(name = "test server", version = "1.0"), - serverOptions, - ) - - // Verify that removing a tool throws an exception - val exception = assertThrows { - server.removeTool("test-tool") - } - assertEquals("Server does not support tools capability.", exception.message) - } - - @Test - fun `removeTools should remove multiple tools`() = runTest { - // Create server with tools capability - val serverOptions = ServerOptions( - capabilities = ServerCapabilities( - tools = ServerCapabilities.Tools(null), - ), - ) - val server = Server( - Implementation(name = "test server", version = "1.0"), - serverOptions, - ) - - // Add tools - server.addTool("test-tool-1", "Test Tool 1") { - CallToolResult(listOf(TextContent("Test result 1"))) - } - server.addTool("test-tool-2", "Test Tool 2") { - CallToolResult(listOf(TextContent("Test result 2"))) - } - - // Setup client - val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() - val client = Client( - clientInfo = Implementation(name = "test client", version = "1.0"), - ) - - // Connect client and server - launch { client.connect(clientTransport) } - launch { server.connect(serverTransport) } - - // Remove the tools - val result = server.removeTools(listOf("test-tool-1", "test-tool-2")) - - // Verify the tools were removed - assertEquals(2, result, "Both tools should be removed") - } - - @Test - fun `removePrompt should remove a prompt`() = runTest { - // Create server with prompts capability - val serverOptions = ServerOptions( - capabilities = ServerCapabilities( - prompts = ServerCapabilities.Prompts(listChanged = false), - ), - ) - val server = Server( - Implementation(name = "test server", version = "1.0"), - serverOptions, - ) - - // Add a prompt - val testPrompt = Prompt("test-prompt", "Test Prompt", null) - server.addPrompt(testPrompt) { - GetPromptResult( - description = "Test prompt description", - messages = listOf(), - ) - } - - // Setup client - val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() - val client = Client( - clientInfo = Implementation(name = "test client", version = "1.0"), - ) - - // Connect client and server - launch { client.connect(clientTransport) } - launch { server.connect(serverTransport) } - - // Remove the prompt - val result = server.removePrompt(testPrompt.name) - - // Verify the prompt was removed - assertTrue(result, "Prompt should be removed successfully") - } - - @Test - fun `removePrompts should remove multiple prompts and send notification`() = runTest { - // Create server with prompts capability - val serverOptions = ServerOptions( - capabilities = ServerCapabilities( - prompts = ServerCapabilities.Prompts(listChanged = false), - ), - ) - val server = Server( - Implementation(name = "test server", version = "1.0"), - serverOptions, - ) - - // Add prompts - val testPrompt1 = Prompt("test-prompt-1", "Test Prompt 1", null) - val testPrompt2 = Prompt("test-prompt-2", "Test Prompt 2", null) - server.addPrompt(testPrompt1) { - GetPromptResult( - description = "Test prompt description 1", - messages = listOf(), - ) - } - server.addPrompt(testPrompt2) { - GetPromptResult( - description = "Test prompt description 2", - messages = listOf(), - ) - } - - // Setup client - val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() - val client = Client( - clientInfo = Implementation(name = "test client", version = "1.0"), - ) - - // Connect client and server - launch { client.connect(clientTransport) } - launch { server.connect(serverTransport) } - - // Remove the prompts - val result = server.removePrompts(listOf(testPrompt1.name, testPrompt2.name)) - - // Verify the prompts were removed - assertEquals(2, result, "Both prompts should be removed") - } - - @Test - fun `removeResource should remove a resource and send notification`() = runTest { - // Create server with resources capability - val serverOptions = ServerOptions( - capabilities = ServerCapabilities( - resources = ServerCapabilities.Resources(null, null), - ), - ) - val server = Server( - Implementation(name = "test server", version = "1.0"), - serverOptions, - ) - - // Add a resource - val testResourceUri = "test://resource" - server.addResource( - uri = testResourceUri, - name = "Test Resource", - description = "A test resource", - mimeType = "text/plain", - ) { - ReadResourceResult( - contents = listOf( - TextResourceContents( - text = "Test resource content", - uri = testResourceUri, - mimeType = "text/plain", - ), - ), - ) - } - - // Setup client - val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() - val client = Client( - clientInfo = Implementation(name = "test client", version = "1.0"), - ) - - // Connect client and server - launch { client.connect(clientTransport) } - launch { server.connect(serverTransport) } - - // Remove the resource - val result = server.removeResource(testResourceUri) - - // Verify the resource was removed - assertTrue(result, "Resource should be removed successfully") - } - - @Test - fun `removeResources should remove multiple resources and send notification`() = runTest { - // Create server with resources capability - val serverOptions = ServerOptions( - capabilities = ServerCapabilities( - resources = ServerCapabilities.Resources(null, null), - ), - ) - val server = Server( - Implementation(name = "test server", version = "1.0"), - serverOptions, - ) - - // Add resources - val testResourceUri1 = "test://resource1" - val testResourceUri2 = "test://resource2" - server.addResource( - uri = testResourceUri1, - name = "Test Resource 1", - description = "A test resource 1", - mimeType = "text/plain", - ) { - ReadResourceResult( - contents = listOf( - TextResourceContents( - text = "Test resource content 1", - uri = testResourceUri1, - mimeType = "text/plain", - ), - ), - ) - } - server.addResource( - uri = testResourceUri2, - name = "Test Resource 2", - description = "A test resource 2", - mimeType = "text/plain", - ) { - ReadResourceResult( - contents = listOf( - TextResourceContents( - text = "Test resource content 2", - uri = testResourceUri2, - mimeType = "text/plain", - ), - ), - ) - } - - // Setup client - val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() - val client = Client( - clientInfo = Implementation(name = "test client", version = "1.0"), - ) - - // Connect client and server - launch { client.connect(clientTransport) } - launch { server.connect(serverTransport) } - - // Remove the resources - val result = server.removeResources(listOf(testResourceUri1, testResourceUri2)) - - // Verify the resources were removed - assertEquals(2, result, "Both resources should be removed") - } - - @Test - fun `removePrompt should return false when prompt does not exist`() = runTest { - // Create server with prompts capability - val serverOptions = ServerOptions( - capabilities = ServerCapabilities( - prompts = ServerCapabilities.Prompts(listChanged = false), - ), - ) - val server = Server( - Implementation(name = "test server", version = "1.0"), - serverOptions, - ) - - // Setup client - val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() - val client = Client( - clientInfo = Implementation(name = "test client", version = "1.0"), - ) - - // Track notifications - var promptListChangedNotificationReceived = false - client.setNotificationHandler(Method.Defined.NotificationsPromptsListChanged) { - promptListChangedNotificationReceived = true - CompletableDeferred(Unit) - } - - // Connect client and server - launch { client.connect(clientTransport) } - launch { server.connect(serverTransport) } - - // Try to remove a non-existent prompt - val result = server.removePrompt("non-existent-prompt") - - // Verify the result - assertFalse(result, "Removing non-existent prompt should return false") - assertFalse(promptListChangedNotificationReceived, "No notification should be sent when prompt doesn't exist") - } - - @Test - fun `removePrompt should throw when prompts capability is not supported`() = runTest { - // Create server without prompts capability - val serverOptions = ServerOptions( - capabilities = ServerCapabilities(), - ) - val server = Server( - Implementation(name = "test server", version = "1.0"), - serverOptions, - ) - - // Verify that removing a prompt throws an exception - val exception = assertThrows { - server.removePrompt("test-prompt") - } - assertEquals("Server does not support prompts capability.", exception.message) - } - - @Test - fun `removeResource should return false when resource does not exist`() = runTest { - // Create server with resources capability - val serverOptions = ServerOptions( - capabilities = ServerCapabilities( - resources = ServerCapabilities.Resources(null, null), - ), - ) - val server = Server( - Implementation(name = "test server", version = "1.0"), - serverOptions, - ) - - // Setup client - val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() - val client = Client( - clientInfo = Implementation(name = "test client", version = "1.0"), - ) - - // Track notifications - var resourceListChangedNotificationReceived = false - client.setNotificationHandler( - Method.Defined.NotificationsResourcesListChanged, - ) { - resourceListChangedNotificationReceived = true - CompletableDeferred(Unit) - } - - // Connect client and server - launch { client.connect(clientTransport) } - launch { server.connect(serverTransport) } - - // Try to remove a non-existent resource - val result = server.removeResource("non-existent-resource") - - // Verify the result - assertFalse(result, "Removing non-existent resource should return false") - assertFalse( - resourceListChangedNotificationReceived, - "No notification should be sent when resource doesn't exist", - ) - } - - @Test - fun `removeResource should throw when resources capability is not supported`() = runTest { - // Create server without resources capability - val serverOptions = ServerOptions( - capabilities = ServerCapabilities(), - ) - val server = Server( - Implementation(name = "test server", version = "1.0"), - serverOptions, - ) - - // Verify that removing a resource throws an exception - val exception = assertThrows { - server.removeResource("test://resource") - } - assertEquals("Server does not support resources capability.", exception.message) - } -} diff --git a/kotlin-sdk-testing/Module.md b/kotlin-sdk-testing/Module.md new file mode 100644 index 0000000..9291de5 --- /dev/null +++ b/kotlin-sdk-testing/Module.md @@ -0,0 +1,67 @@ +# Module kotlin-sdk-testing + +`kotlin-sdk-testing` provides testing utilities for MCP client-server interactions. It eliminates the need for external +processes or network connections by offering coroutine-safe in-memory transport implementations that connect clients and servers directly. + +## Typical usage + +1. Create linked transport pair with `ChannelTransport.createLinkedPair()`. +2. Connect client and server concurrently using the paired transports. +3. Configure server request handlers. +4. Execute client operations and verify results. + +### Complete test example + +```kotlin +@OptIn(ExperimentalMcpApi::class) +@Test +fun testClientServerCommunication(): Unit = runBlocking { + val server = Server( + Implementation(name = "test server", version = "1.0"), + ServerOptions(capabilities = ServerCapabilities(resources = ServerCapabilities.Resources())), + ) + + val (clientTransport, serverTransport) = ChannelTransport.createLinkedPair() + + val client = Client(clientInfo = Implementation(name = "test client", version = "1.0")) + + val serverSessionResult = CompletableDeferred() + listOf( + launch { client.connect(clientTransport) }, + launch { serverSessionResult.complete(server.createSession(serverTransport)) }, + ).joinAll() + + val serverSession = serverSessionResult.await() + serverSession.setRequestHandler(Method.Defined.Initialize) { _, _ -> + InitializeResult( + protocolVersion = LATEST_PROTOCOL_VERSION, + capabilities = ServerCapabilities( + resources = ServerCapabilities.Resources(null, null), + tools = ServerCapabilities.Tools(null), + ), + serverInfo = Implementation(name = "test", version = "1.0"), + ) + } + + serverSession.setRequestHandler(Method.Defined.ResourcesList) { _, _ -> + ListResourcesResult( + resources = listOf(Resource(uri = "/foo/bar", name = "foo-bar-resource")), + ) + } + + // Test client operations + client.listResources() shouldNotBeNull { + this.resources shouldHaveSize 1 + } + + // Clean up resources + client.close() + server.close() +} +``` + +**Note**: In production tests, wrap client/server operations in try-finally blocks to ensure proper resource cleanup even if assertions fail. + +Use this module for unit and integration testing of MCP implementations without managing external server processes, +network ports, or I/O streams. The channel-based transport ensures tests run instantly with full isolation between test +cases. diff --git a/kotlin-sdk-testing/api/kotlin-sdk-testing.api b/kotlin-sdk-testing/api/kotlin-sdk-testing.api new file mode 100644 index 0000000..4a942a4 --- /dev/null +++ b/kotlin-sdk-testing/api/kotlin-sdk-testing.api @@ -0,0 +1,26 @@ +public final class io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransport { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport$Companion; + public fun (Lkotlinx/coroutines/channels/Channel;Lkotlinx/coroutines/CoroutineDispatcher;)V + public synthetic fun (Lkotlinx/coroutines/channels/Channel;Lkotlinx/coroutines/CoroutineDispatcher;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lkotlinx/coroutines/channels/SendChannel;Lkotlinx/coroutines/channels/ReceiveChannel;Lkotlinx/coroutines/CoroutineDispatcher;)V + public synthetic fun (Lkotlinx/coroutines/channels/SendChannel;Lkotlinx/coroutines/channels/ReceiveChannel;Lkotlinx/coroutines/CoroutineDispatcher;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport$Companion { + public final fun createLinkedPair (ILkotlinx/coroutines/CoroutineDispatcher;)Lio/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport$LinkedTransports; + public static synthetic fun createLinkedPair$default (Lio/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport$Companion;ILkotlinx/coroutines/CoroutineDispatcher;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport$LinkedTransports; +} + +public final class io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport$LinkedTransports { + public fun (Lio/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport;Lio/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport; + public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport;Lio/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport;)Lio/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport$LinkedTransports; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport$LinkedTransports;Lio/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport;Lio/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport$LinkedTransports; + public fun equals (Ljava/lang/Object;)Z + public final fun getClientTransport ()Lio/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport; + public final fun getServerTransport ()Lio/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + diff --git a/kotlin-sdk-testing/build.gradle.kts b/kotlin-sdk-testing/build.gradle.kts new file mode 100644 index 0000000..bbb0388 --- /dev/null +++ b/kotlin-sdk-testing/build.gradle.kts @@ -0,0 +1,52 @@ +@file:OptIn(ExperimentalWasmDsl::class) + +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl + +plugins { + id("mcp.multiplatform") + id("mcp.detekt") + id("mcp.publishing") + id("mcp.dokka") + alias(libs.plugins.kotlinx.binary.compatibility.validator) +} + +kotlin { + + iosArm64() + iosX64() + iosSimulatorArm64() + watchosArm64() + watchosSimulatorArm64() + tvosArm64() + tvosSimulatorArm64() + js { + browser() + nodejs() + } + wasmJs { + browser() + nodejs() + } + + sourceSets { + commonMain { + dependencies { + api(project(":kotlin-sdk-core")) + implementation(libs.kotlin.logging) + } + } + + commonTest { + dependencies { + implementation(kotlin("test")) + implementation(libs.kotlinx.coroutines.test) + implementation(libs.kotest.assertions.core) + } + } + jvmTest { + dependencies { + runtimeOnly(libs.slf4j.simple) + } + } + } +} diff --git a/kotlin-sdk-testing/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport.kt b/kotlin-sdk-testing/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport.kt new file mode 100644 index 0000000..ae5f487 --- /dev/null +++ b/kotlin-sdk-testing/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport.kt @@ -0,0 +1,182 @@ +package io.modelcontextprotocol.kotlin.sdk.testing + +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.shared.AbstractClientTransport +import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.launch +import kotlinx.coroutines.yield + +/** + * A transport implementation that uses Kotlin Coroutines Channels for asynchronous + * communication of JSON-RPC messages. This is useful in scenarios where communication is + * message-based and channels are used as the underlying mechanism for delivery. + * + * @param sendChannel The channel used to send JSON-RPC messages. + * @param receiveChannel The channel used to receive JSON-RPC messages. + * @param dispatcher The coroutine dispatcher for the event loop. Defaults to [Dispatchers.Default]. + */ +@ExperimentalMcpApi +public class ChannelTransport( + private val sendChannel: SendChannel, + private val receiveChannel: ReceiveChannel, + dispatcher: CoroutineDispatcher = Dispatchers.Default, +) : AbstractClientTransport() { + + override val logger: KLogger = KotlinLogging.logger {} + + private val scope = CoroutineScope(SupervisorJob() + dispatcher) + + /** + * Creates a `ChannelTransport` instance using a single channel for both sending and receiving messages. + * + * This constructor simplifies the use of `ChannelTransport` in scenarios where a single channel + * is enough for bidirectional communication. A default channel with unlimited capacity is used + * if no channel is provided. + * + * @param channel The `Channel` of `JSONRPCMessage` instances, used for both sending and receiving. + * Defaults to a channel with unlimited capacity. + * @param dispatcher The coroutine dispatcher for the event loop. Defaults to [Dispatchers.Default]. + */ + public constructor( + channel: Channel = Channel(UNLIMITED), + dispatcher: CoroutineDispatcher = Dispatchers.Default, + ) : this(channel, channel, dispatcher) + + /** + * Represents a pair of interconnected [ChannelTransport]s for bidirectional communication. + * + * Messages sent via one transport are received by the other. This is useful for + * setting up communication between a client and server without requiring external networking. + * + * @property clientTransport The transport intended for use on the client-side. + * @property serverTransport The transport intended for use on the server-side. + */ + public data class LinkedTransports(val clientTransport: ChannelTransport, val serverTransport: ChannelTransport) + + public companion object { + + /** + * Creates a pair of interconnected [ChannelTransport] objects for bidirectional communication. + * + * Sets up a client and server transport using Kotlin channels, so that messages sent through one + * transport are received by the other. Typically used for testing or in-memory communication + * without external networking. + * + * Example: + * ```kotlin + * val (clientTransport, serverTransport) = ChannelTransport.createLinkedPair() + * server.connect(serverTransport) + * client.connect(clientTransport) + * ``` + * + * @param capacity The buffer capacity of the internal channels used for communication. Defaults to 256. + * @param dispatcher The coroutine dispatcher for the event loop. Defaults to [Dispatchers.Default]. + * @return A [LinkedTransports] instance containing the client and server transports. + */ + public fun createLinkedPair( + capacity: Int = 256, + dispatcher: CoroutineDispatcher = Dispatchers.Default, + ): LinkedTransports { + val sendChannel = Channel(capacity) + val receiveChannel = Channel(capacity) + val clientTransport = ChannelTransport(sendChannel, receiveChannel, dispatcher) + val serverTransport = ChannelTransport(receiveChannel, sendChannel, dispatcher) + return LinkedTransports(clientTransport = clientTransport, serverTransport = serverTransport) + } + } + + /** + * Starts the transport, allowing it to begin processing messages. + * + * Waits for the event loop to start before returning, ensuring the transport is + * fully initialized and ready to send/receive messages. + * + * Processes messages until the channel closes or cancellation occurs. + * Exceptions from message handlers are reported via onError but do not stop processing. + */ + override suspend fun initialize() { + logger.info { "ChannelTransport starting message processing" } + val started = CompletableDeferred() + scope.launch(CoroutineName("ChannelTransport#${hashCode()}-event-loop")) { + try { + // Signal that event loop has started, then yield to ensure we're ready + started.complete(Unit) + yield() + + for (message in receiveChannel) { + logger.debug { "Received message: ${message::class.simpleName}" } + + try { + _onMessage.invoke(message) + logger.trace { "Message processed successfully: ${message::class.simpleName}" } + } catch (e: CancellationException) { + // Let cancellation propagate immediately + logger.debug { "Cancellation requested during message processing" } + throw e + } catch (e: Exception) { + // Report other errors but continue processing + logger.warn(e) { "Error processing message: ${message::class.simpleName}" } + _onError.invoke(e) + } + } + logger.info { "ChannelTransport stopped: receive channel closed" } + } catch (e: Exception) { + // Only complete exceptionally if not already completed + if (!started.isCompleted) { + started.completeExceptionally(e) + } + throw e + } finally { + close() + } + } + // Wait for the event loop to start + started.await() + } + + /** + * Sends a JSON-RPC message through the transport. + * + * @param message The JSON-RPC message to send. This can be a request or a response, + * conforming to the JSON-RPC 2.0 specification. + * @param options Optional transport-specific options that provide additional context or behavior + * for the message transmission, such as associating the message with a specific + * incoming request or handling resumption tokens. + */ + override suspend fun performSend(message: JSONRPCMessage, options: TransportSendOptions?) { + logger.debug { "Sending message: ${message::class.simpleName}" } + sendChannel.send(message) + logger.trace { "Message sent successfully: ${message::class.simpleName}" } + } + + /** + * Closes the transport, preventing further message transmission and processing. + */ + override suspend fun closeResources() { + logger.info { "Closing ChannelTransport" } + sendChannel.close() + if (receiveChannel !== sendChannel) { + logger.debug { "Cancelling separate receive channel" } + receiveChannel.cancel() + } + scope.cancel() + scope.coroutineContext[Job]?.join() // Wait for cleanup + logger.info { "ChannelTransport closed" } + } +} diff --git a/kotlin-sdk-testing/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransportTest.kt b/kotlin-sdk-testing/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransportTest.kt new file mode 100644 index 0000000..4aae932 --- /dev/null +++ b/kotlin-sdk-testing/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransportTest.kt @@ -0,0 +1,211 @@ +package io.modelcontextprotocol.kotlin.sdk.testing + +import io.kotest.assertions.nondeterministic.eventually +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest +import io.modelcontextprotocol.kotlin.sdk.types.RequestId +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ClosedSendChannelException +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.time.Duration.Companion.seconds + +@OptIn(ExperimentalMcpApi::class) +class ChannelTransportTest { + + @Test + fun `send writes to sendChannel`() = runTest { + val sendChannel = Channel(Channel.UNLIMITED) + val transport = ChannelTransport(sendChannel, Channel()) + + transport.start() + + val message = JSONRPCRequest(RequestId.NumberId(42), "test") + transport.send(message) + + sendChannel.receive() shouldBe message + } + + @Test + fun `start reads from receiveChannel and invokes onMessage`() = runTest { + val receiveChannel = Channel(Channel.UNLIMITED) + val transport = ChannelTransport(Channel(), receiveChannel) + + val messagesProcessed = CompletableDeferred() + val received = mutableListOf() + transport.onMessage { msg -> + received.add(msg) + if (received.size == 2) { + messagesProcessed.complete(Unit) + } + } + + transport.start() + + val msg1 = JSONRPCRequest(RequestId.NumberId(1), "method1") + val msg2 = JSONRPCRequest(RequestId.NumberId(2), "method2") + receiveChannel.send(msg1) + receiveChannel.send(msg2) + + // Wait for messages to be processed + messagesProcessed.await() + + received.shouldContainExactly(msg1, msg2) + } + + @Test + fun `start completes when channel closes`() = runTest { + val receiveChannel = Channel(Channel.UNLIMITED) + val transport = ChannelTransport(Channel(), receiveChannel) + + val messageProcessed = CompletableDeferred() + val received = mutableListOf() + transport.onMessage { + received.add(it) + messageProcessed.complete(Unit) + } + + transport.start() + + val msg = JSONRPCRequest(RequestId.NumberId(1), "method1") + receiveChannel.send(msg) + receiveChannel.close() // Close the channel + + // Wait for a message to be processed + messageProcessed.await() + + received.shouldContainExactly(msg) + } + + @Test + fun `close is idempotent`() = runTest { + val transport = ChannelTransport() + + var closeCount = 0 + transport.onClose { closeCount++ } + + transport.start() + + transport.close() + transport.close() + + closeCount shouldBe 1 + } + + @Test + fun `callbacks chain not replace`() = runTest { + val receiveChannel = Channel(Channel.UNLIMITED) + val transport = ChannelTransport(Channel(), receiveChannel) + + val messageProcessed = CompletableDeferred() + val calls = mutableListOf() + transport.onMessage { + calls.add("first") + } + transport.onMessage { + calls.add("second") + messageProcessed.complete(Unit) + } + + val startJob = backgroundScope.launch { transport.start() } + + receiveChannel.send(JSONRPCRequest(RequestId.NumberId(1), "test")) + messageProcessed.await() + + calls.shouldContainExactly("first", "second") + startJob.cancelAndJoin() + } + + @Test + fun `secondary constructor uses single channel`() = runTest { + val channel = Channel(Channel.UNLIMITED) + val transport = ChannelTransport(channel) + + val messageProcessed = CompletableDeferred() + val received = mutableListOf() + transport.onMessage { msg -> + received.add(msg) + messageProcessed.complete(Unit) + } + + transport.start() + + val message = JSONRPCRequest(RequestId.NumberId(1), "test") + transport.send(message) + messageProcessed.await() + + eventually(2.seconds) { + received.shouldContainExactly(message) + } + } + + @Test + fun `send to closed channel triggers error`() = runTest { + val sendChannel = Channel(Channel.UNLIMITED) + val transport = ChannelTransport(sendChannel, Channel()) + + transport.start() + + var errorCaught = false + transport.onError { + it.shouldBeInstanceOf() + errorCaught = true + } + sendChannel.close() + + try { + transport.send(JSONRPCRequest(RequestId.NumberId(1), "method")) + } catch (e: Exception) { + // send() wraps ClosedSendChannelException in McpException + e.shouldBeInstanceOf() + } + + eventually(2.seconds) { + errorCaught shouldBe true + } + } + + @Test + fun `exceptions in message handler do not stop processing`() = runTest { + val receiveChannel = Channel(Channel.UNLIMITED) + val transport = ChannelTransport(Channel(), receiveChannel) + + val allProcessed = CompletableDeferred() + val received = mutableListOf() + val errors = mutableListOf() + + transport.onError { errors.add(it) } + transport.onMessage { msg -> + val id = ((msg as JSONRPCRequest).id as RequestId.NumberId).value.toInt() + received.add(id) + if (id == 2) { + throw RuntimeException("Error processing message 2") + } + if (received.size == 4) { + allProcessed.complete(Unit) + } + } + + transport.start() + + // Send 4 messages, second one will throw + receiveChannel.send(JSONRPCRequest(RequestId.NumberId(1), "m1")) + receiveChannel.send(JSONRPCRequest(RequestId.NumberId(2), "m2")) + receiveChannel.send(JSONRPCRequest(RequestId.NumberId(3), "m3")) + receiveChannel.send(JSONRPCRequest(RequestId.NumberId(4), "m4")) + + allProcessed.await() + + // All messages should be processed despite error in message 2 + received.shouldContainExactly(1, 2, 3, 4) + errors.size shouldBe 1 + errors[0].message shouldBe "Error processing message 2" + } +} diff --git a/kotlin-sdk-testing/src/jvmTest/resources/junit-platform.properties b/kotlin-sdk-testing/src/jvmTest/resources/junit-platform.properties new file mode 100644 index 0000000..f14d100 --- /dev/null +++ b/kotlin-sdk-testing/src/jvmTest/resources/junit-platform.properties @@ -0,0 +1,5 @@ +## https://docs.junit.org/5.3.0-M1/user-guide/index.html#writing-tests-parallel-execution +junit.jupiter.execution.parallel.enabled=true +junit.jupiter.execution.parallel.config.strategy=dynamic +junit.jupiter.execution.parallel.mode.default=concurrent +junit.jupiter.execution.parallel.mode.classes.default=concurrent diff --git a/kotlin-sdk-testing/src/jvmTest/resources/simplelogger.properties b/kotlin-sdk-testing/src/jvmTest/resources/simplelogger.properties new file mode 100644 index 0000000..5679351 --- /dev/null +++ b/kotlin-sdk-testing/src/jvmTest/resources/simplelogger.properties @@ -0,0 +1,7 @@ +# Level of logging for the ROOT logger: ERROR, WARN, INFO, DEBUG, TRACE (default is INFO) +org.slf4j.simpleLogger.defaultLogLevel=INFO +org.slf4j.simpleLogger.showThreadName=true +org.slf4j.simpleLogger.showDateTime=false + +# Log level for specific packages or classes +org.slf4j.simpleLogger.log.io.modelcontextprotocol=DEBUG diff --git a/kotlin-sdk/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/package.kt b/kotlin-sdk/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/package.kt index 59faee4..13bf272 100644 --- a/kotlin-sdk/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/package.kt +++ b/kotlin-sdk/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/package.kt @@ -1,25 +1,3 @@ -@file:Suppress("ktlint:standard:no-empty-class-body", "ktlint:standard:kdoc") -/** - * # MCP Kotlin SDK - * - * A Kotlin Multiplatform implementation of the Model Context Protocol (MCP). - * - * This is the main SDK module that provides a convenient single dependency - * for all MCP functionality including: - * - * - Core protocol types and utilities ([kotlin-sdk-core]) - * - Client implementations ([kotlin-sdk-client]) - * - Server implementations ([kotlin-sdk-server]) - * - * ## Usage - * - * Add this dependency to your project to get access to all MCP Kotlin SDK functionality: - * - * ```kotlin - * implementation("io.modelcontextprotocol:kotlin-sdk:$version") - * ``` - * - * This will transitively include all core, client, and server components. - */ - package io.modelcontextprotocol.kotlin.sdk + +private class Stub diff --git a/samples/README.md b/samples/README.md new file mode 100644 index 0000000..f831a74 --- /dev/null +++ b/samples/README.md @@ -0,0 +1,63 @@ +# Kotlin MCP SDK Samples + +Runnable projects demonstrating MCP server and client implementations with the +[Kotlin MCP SDK](https://github.com/modelcontextprotocol/kotlin-sdk). +For background on the protocol itself, see the [MCP documentation](https://modelcontextprotocol.io/introduction). + +## Overview + +| Sample | Type | Transport | MCP Features | +|--------------------------------------------------------|-------------------|-----------------|------------------------------------| +| [simple-streamable-server](./simple-streamable-server) | Server | Streamable HTTP | Tools, Resources, Prompts, Logging | +| [kotlinlang-mcp-server](./kotlinlang-mcp-server) | Server | Streamable HTTP | Tools | +| [kotlin-mcp-server](./kotlin-mcp-server) | Server | STDIO, SSE | Tools, Resources, Prompts | +| [weather-stdio-server](./weather-stdio-server) | Server | STDIO | Tools | +| [kotlin-mcp-client](./kotlin-mcp-client) | Client | STDIO | Tool discovery & invocation | +| [notebooks](./notebooks) | Client (Notebook) | Streamable HTTP | Tool discovery & invocation | + +## Getting Started + +- **Building a server?** Start with [simple-streamable-server](./simple-streamable-server) — it + uses the recommended Streamable HTTP transport and covers tools, resources, prompts, and logging. +- **Building a client?** Open the [notebooks](./notebooks) sample for a step-by-step walkthrough, + or see [kotlin-mcp-client](./kotlin-mcp-client) for a full CLI client with Anthropic API + integration. + +## Samples + +### Simple Streamable HTTP Server + +A minimal Streamable HTTP server with optional Bearer token authentication. Demonstrates tools +(`greet`, `multi-greet`), a prompt template, a resource, and server-to-client logging notifications. +[Read more →](./simple-streamable-server) + +### Kotlinlang MCP Server + +A Streamable HTTP server that exposes the official Kotlin documentation (kotlinlang.org) to LLM +clients — full-text search via Algolia and page retrieval in markdown. Demonstrates wrapping a real +external API in an MCP server with in-memory caching. +[Read more →](./kotlinlang-mcp-server) + +### Kotlin MCP Server + +A multi-transport server supporting STDIO, SSE (plain), and SSE (Ktor plugin). Useful for exploring +different transport modes side by side. +[Read more →](./kotlin-mcp-server) + +### Weather STDIO Server + +A focused STDIO server that exposes weather forecast and alert tools backed by the weather.gov API. +Includes Claude Desktop integration instructions. +[Read more →](./weather-stdio-server) + +### Kotlin MCP Client + +An interactive CLI client that connects to any MCP server over STDIO and routes queries through +Anthropic's Claude API, bridging MCP tools with LLM conversations. +[Read more →](./kotlin-mcp-client) + +### MCP Client Notebook + +A Kotlin notebook that connects to a remote MCP server via Streamable HTTP and demonstrates ping, +tool listing, and tool invocation — all in an interactive cell-by-cell format. +[Read more →](./notebooks) diff --git a/samples/kotlin-mcp-client/README.md b/samples/kotlin-mcp-client/README.md index 3842ea0..b10f925 100644 --- a/samples/kotlin-mcp-client/README.md +++ b/samples/kotlin-mcp-client/README.md @@ -1,69 +1,50 @@ # Kotlin MCP Client -This project demonstrates how to build a Model Context Protocol (MCP) client in Kotlin that interacts with an MCP server -via a STDIO transport layer while leveraging Anthropic's API for natural language processing. The client uses the MCP -Kotlin SDK to communicate with an MCP server that exposes various tools, and it uses Anthropic's API to process user -queries and integrate tool responses into the conversation. - -For more information about the MCP SDK and protocol, please refer to -the [MCP documentation](https://modelcontextprotocol.io/introduction). - -## Prerequisites - -- **Java 17 or later** -- **Gradle** (or the Gradle wrapper provided with the project) -- An Anthropic API key set in your environment variable `ANTHROPIC_API_KEY` -- Basic understanding of MCP concepts and Kotlin programming +An interactive CLI client that connects to any MCP server over STDIO and pipes queries through +Anthropic's Claude API. ## Overview -The client application performs the following tasks: +This sample demonstrates a complete MCP client workflow: launching an MCP server as a subprocess, +discovering its tools, converting them to Anthropic's tool format, and running an interactive chat +loop where Claude can call server tools on behalf of the user. -- **Connecting to an MCP server** — - launches an MCP server process (implemented in JavaScript, Python, or Java) using STDIO transport. - It connects to the server, retrieves available tools, and converts them to Anthropic’s tool format. -- **Processing queries** — - accepts user queries, sends them to Anthropic’s API along with the registered tools, and handles responses. - If the response indicates a tool should be called, it invokes the corresponding MCP tool and continues the - conversation based on the tool’s result. -- **Interactive chat loop** — - runs an interactive command-line loop, allowing users to continuously submit queries and receive responses. +## Prerequisites -## Building and Running +- JDK 17+ +- An `ANTHROPIC_API_KEY` environment variable set with a valid Anthropic API key +- An MCP server script to connect to (`.js`, `.py`, or `.jar`) -Use the Gradle wrapper to build the application. In a terminal, run: +## Build & Run + +Run the client, passing the path to an MCP server: ```shell -./gradlew clean build -x test -``` +# Connect to a JVM server +./gradlew run --args="path/to/server.jar" -To run the client, execute the jar file and provide the path to your MCP server script. +# Connect to a Python server +./gradlew run --args="path/to/server.py" -To run the client with any MCP server: - -```shell -java -jar build/libs/.jar path/to/server.jar # jvm server -java -jar build/libs/.jar path/to/server.py # python server -java -jar build/libs/.jar path/to/build/index.js # node server +# Connect to a Node.js server +./gradlew run --args="path/to/build/index.js" ``` > [!NOTE] -> The client uses STDIO transport, so it launches the MCP server as a separate process. +> The client uses STDIO transport, so it launches the MCP server as a subprocess. > Ensure the server script is executable and is a valid `.js`, `.py`, or `.jar` file. -## Configuration for Anthropic +## MCP Capabilities -Ensure your Anthropic API key is available in your environment: +From the **client** perspective, this sample demonstrates: -```shell -export ANTHROPIC_API_KEY=your_anthropic_api_key_here -``` - -The client uses `AnthropicOkHttpClient.fromEnv()` to automatically load the API key from `ANTHROPIC_API_KEY` and -`ANTHROPIC_AUTH_TOKEN` environment variables. +- **Tool discovery** — lists tools from the connected server and converts them to Anthropic's tool + format. +- **Tool invocation** — when Claude's response requests a tool call, the client invokes the + corresponding MCP tool and feeds the result back into the conversation. ## Additional Resources -- [MCP Specification](https://spec.modelcontextprotocol.io/) +- [MCP Specification](https://modelcontextprotocol.io/specification/latest) - [Kotlin MCP SDK](https://github.com/modelcontextprotocol/kotlin-sdk) -- [Anthropic Java SDK](https://github.com/anthropics/anthropic-sdk-java/tree/main) +- [Anthropic Java SDK](https://github.com/anthropics/anthropic-sdk-java) diff --git a/samples/kotlin-mcp-client/build.gradle.kts b/samples/kotlin-mcp-client/build.gradle.kts index 3e3ad85..61774cb 100644 --- a/samples/kotlin-mcp-client/build.gradle.kts +++ b/samples/kotlin-mcp-client/build.gradle.kts @@ -12,6 +12,7 @@ group = "org.example" version = "0.1.0" dependencies { + implementation(dependencies.platform(libs.ktor.bom)) implementation(libs.mcp.kotlin.client) implementation(libs.ktor.client.cio) implementation(libs.anthropic.java) diff --git a/samples/kotlin-mcp-client/gradle.properties b/samples/kotlin-mcp-client/gradle.properties new file mode 100644 index 0000000..fdc17e4 --- /dev/null +++ b/samples/kotlin-mcp-client/gradle.properties @@ -0,0 +1,6 @@ +org.gradle.configuration-cache=true +org.gradle.parallel=true +org.gradle.caching=true + +#mcp.kotlin.overrideVersion=0.0.1-SNAPSHOT + diff --git a/samples/kotlin-mcp-client/gradle/libs.versions.toml b/samples/kotlin-mcp-client/gradle/libs.versions.toml new file mode 100644 index 0000000..b58eef2 --- /dev/null +++ b/samples/kotlin-mcp-client/gradle/libs.versions.toml @@ -0,0 +1,18 @@ +[versions] +anthropic = "2.62.0" +kotlin = "2.4.20" +ktor = "3.5.2" +mcp-kotlin = "0.15.0" +shadow = "9.6.1" +slf4j = "2.0.19" + +[libraries] +anthropic-java = { group = "com.anthropic", name = "anthropic-java", version.ref = "anthropic" } +ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" } +ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio" } +mcp-kotlin-client = { group = "io.modelcontextprotocol", name = "kotlin-sdk-client", version.ref = "mcp-kotlin" } +slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } + +[plugins] +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } diff --git a/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.jar b/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..eddabd2 Binary files /dev/null and b/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.jar differ diff --git a/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.properties b/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ad7845b --- /dev/null +++ b/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/samples/kotlin-mcp-client/gradlew b/samples/kotlin-mcp-client/gradlew new file mode 100755 index 0000000..249efbb --- /dev/null +++ b/samples/kotlin-mcp-client/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/samples/kotlin-mcp-client/gradlew.bat b/samples/kotlin-mcp-client/gradlew.bat new file mode 100644 index 0000000..8508ef6 --- /dev/null +++ b/samples/kotlin-mcp-client/gradlew.bat @@ -0,0 +1,82 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/samples/kotlin-mcp-client/settings.gradle.kts b/samples/kotlin-mcp-client/settings.gradle.kts index c5f225b..7b54e1d 100644 --- a/samples/kotlin-mcp-client/settings.gradle.kts +++ b/samples/kotlin-mcp-client/settings.gradle.kts @@ -1,10 +1,8 @@ rootProject.name = "kotlin-mcp-client" -pluginManagement { - repositories { - mavenCentral() - gradlePluginPortal() - } +plugins { + // Apply the foojay-resolver plugin to allow automatic download of JDKs + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" } dependencyResolutionManagement { @@ -12,9 +10,16 @@ dependencyResolutionManagement { mavenLocal() mavenCentral() } + versionCatalogs { create("libs") { - from(files("../../gradle/libs.versions.toml")) + val mcpKotlinVersion = providers.gradleProperty( + "mcp.kotlin.overrideVersion", + ).orNull + if (mcpKotlinVersion != null) { + logger.lifecycle("Using the override version $mcpKotlinVersion of MCP Kotlin SDK") + version("mcp-kotlin", mcpKotlinVersion) + } } } } diff --git a/samples/kotlin-mcp-client/src/main/kotlin/io/modelcontextprotocol/sample/client/MCPClient.kt b/samples/kotlin-mcp-client/src/main/kotlin/io/modelcontextprotocol/sample/client/MCPClient.kt index d9d482d..23fdfcc 100644 --- a/samples/kotlin-mcp-client/src/main/kotlin/io/modelcontextprotocol/sample/client/MCPClient.kt +++ b/samples/kotlin-mcp-client/src/main/kotlin/io/modelcontextprotocol/sample/client/MCPClient.kt @@ -2,34 +2,41 @@ package io.modelcontextprotocol.sample.client import com.anthropic.client.okhttp.AnthropicOkHttpClient import com.anthropic.core.JsonValue +import com.anthropic.models.messages.ContentBlockParam import com.anthropic.models.messages.MessageCreateParams import com.anthropic.models.messages.MessageParam -import com.anthropic.models.messages.Model import com.anthropic.models.messages.Tool +import com.anthropic.models.messages.ToolResultBlockParam import com.anthropic.models.messages.ToolUnion import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.ObjectMapper -import io.modelcontextprotocol.kotlin.sdk.Implementation -import io.modelcontextprotocol.kotlin.sdk.TextContent import io.modelcontextprotocol.kotlin.sdk.client.Client import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport +import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext import kotlinx.io.asSink import kotlinx.io.asSource import kotlinx.io.buffered import kotlinx.serialization.json.JsonObject -import kotlin.jvm.optionals.getOrNull +import java.util.concurrent.TimeUnit -class MCPClient : AutoCloseable { - // Configures using the `ANTHROPIC_API_KEY` and `ANTHROPIC_AUTH_TOKEN` environment variables - private val anthropic = AnthropicOkHttpClient.fromEnv() +private const val MODEL = "claude-sonnet-4-20250514" +private const val MAX_TOKENS = 1024L + +class MCPClient(apiKey: String) : AutoCloseable { + private val anthropic = AnthropicOkHttpClient.builder() + .apiKey(apiKey) + .build() // Initialize MCP client private val mcp: Client = Client(clientInfo = Implementation(name = "mcp-client-cli", version = "1.0.0")) - private val messageParamsBuilder: MessageCreateParams.Builder = MessageCreateParams.builder() - .model(Model.CLAUDE_4_SONNET_20250514) - .maxTokens(1024) + // Server process reference for cleanup + private var serverProcess: Process? = null // List of tools offered by the server private lateinit var tools: List @@ -42,52 +49,52 @@ class MCPClient : AutoCloseable { // Connect to the server using the path to the server suspend fun connectToServer(serverScriptPath: String) { - try { - // Build the command based on the file extension of the server script - val command = buildList { - when (serverScriptPath.substringAfterLast(".")) { - "js" -> add("node") - "py" -> add(if (System.getProperty("os.name").lowercase().contains("win")) "python" else "python3") - "jar" -> addAll(listOf("java", "-jar")) - else -> throw IllegalArgumentException("Server script must be a .js, .py or .jar file") - } - add(serverScriptPath) + // Build the command based on the file extension of the server script + val command = buildList { + when (serverScriptPath.substringAfterLast(".")) { + "js" -> add("node") + "py" -> add(if (System.getProperty("os.name").lowercase().contains("win")) "python" else "python3") + "jar" -> addAll(listOf("java", "-jar")) + else -> throw IllegalArgumentException("Server script must be a .js, .py or .jar file") } - - // Start the server process - val process = ProcessBuilder(command).start() - - // Setup I/O transport using the process streams - val transport = StdioClientTransport( - input = process.inputStream.asSource().buffered(), - output = process.outputStream.asSink().buffered(), - ) - - // Connect the MCP client to the server using the transport - mcp.connect(transport) - - // Request the list of available tools from the server - val toolsResult = mcp.listTools() - tools = toolsResult.tools.map { tool -> - ToolUnion.ofTool( - Tool.builder() - .name(tool.name) - .description(tool.description ?: "") - .inputSchema( - Tool.InputSchema.builder() - .type(JsonValue.from(tool.inputSchema.type)) - .properties(tool.inputSchema.properties.toJsonValue()) - .putAdditionalProperty("required", JsonValue.from(tool.inputSchema.required)) - .build(), - ) - .build(), - ) - } - println("Connected to server with tools: ${tools.joinToString(", ") { it.tool().get().name() }}") - } catch (e: Exception) { - println("Failed to connect to MCP server: $e") - throw e + add(serverScriptPath) } + + // Start the server process + val process = withContext(Dispatchers.IO) { + ProcessBuilder(command) + .redirectError(ProcessBuilder.Redirect.INHERIT) + .start() + } + serverProcess = process + + // Setup I/O transport using the process streams + val transport = StdioClientTransport( + input = process.inputStream.asSource().buffered(), + output = process.outputStream.asSink().buffered(), + ) + + // Connect the MCP client to the server using the transport + mcp.connect(transport) + + // Request the list of available tools from the server + val toolsResult = mcp.listTools() + tools = toolsResult.tools.map { tool -> + ToolUnion.ofTool( + Tool.builder() + .name(tool.name) + .description(tool.description ?: "") + .inputSchema( + Tool.InputSchema.builder() + .type(JsonValue.from(tool.inputSchema.type)) + .properties(tool.inputSchema.properties?.toJsonValue() ?: EmptyJsonObject.toJsonValue()) + .putAdditionalProperty("required", JsonValue.from(tool.inputSchema.required)) + .build(), + ) + .build(), + ) + } + println("Connected to server with tools: ${tools.joinToString(", ") { it.tool().get().name() }}") } // Process a user query and return a string response @@ -102,23 +109,29 @@ class MCPClient : AutoCloseable { // Send the query to the Anthropic model and get the response val response = anthropic.messages().create( - messageParamsBuilder + MessageCreateParams.builder() + .model(MODEL) + .maxTokens(MAX_TOKENS) .messages(messages) .tools(tools) .build(), ) val finalText = mutableListOf() + val toolResults = mutableListOf() + response.content().forEach { content -> when { // Append text outputs from the response - content.isText() -> finalText.add(content.text().getOrNull()?.text() ?: "") + content.isText() -> finalText.add(content.text().get().text()) // If the response indicates a tool use, process it further content.isToolUse() -> { - val toolName = content.toolUse().get().name() + val toolUse = content.toolUse().get() + val toolName = toolUse.name() + val toolUseId = toolUse.id() val toolArgs = - content.toolUse().get()._input().convert(object : TypeReference>() {}) + toolUse._input().convert(object : TypeReference>() {}) // Call the tool with provided arguments val result = mcp.callTool( @@ -127,35 +140,55 @@ class MCPClient : AutoCloseable { ) finalText.add("[Calling tool $toolName with args $toolArgs]") - // Add the tool result message to the conversation - messages.add( - MessageParam.builder() - .role(MessageParam.Role.USER) - .content( - """ - "type": "tool_result", - "tool_name": $toolName, - "result": ${result?.content?.joinToString("\n") { - (it as TextContent).text ?: "" - }} - """.trimIndent(), - ) - .build(), - ) + // Build tool_result content block with tool_use_id + val toolResultContent = result.content + .filterIsInstance() + .joinToString("\n") { it.text } - // Retrieve an updated response after tool execution - val aiResponse = anthropic.messages().create( - messageParamsBuilder - .messages(messages) - .build(), - ) + val toolResultBlock = ToolResultBlockParam.builder() + .toolUseId(toolUseId) + .content(toolResultContent) + .apply { if (result.isError == true) isError(true) } + .build() - // Append the updated response to final text - finalText.add(aiResponse.content().first().text().getOrNull()?.text() ?: "") + toolResults.add(ContentBlockParam.ofToolResult(toolResultBlock)) } } } + // If there were tool calls, send tool results back and get final response + if (toolResults.isNotEmpty()) { + // Add the full assistant response (includes tool_use blocks) + messages.add( + MessageParam.builder() + .role(MessageParam.Role.ASSISTANT) + .contentOfBlockParams(response.content().map { it.toParam() }) + .build(), + ) + + // Add user message with tool results + messages.add( + MessageParam.builder() + .role(MessageParam.Role.USER) + .contentOfBlockParams(toolResults) + .build(), + ) + + // Retrieve an updated response after tool execution (without tools) + val aiResponse = anthropic.messages().create( + MessageCreateParams.builder() + .model(MODEL) + .maxTokens(MAX_TOKENS) + .messages(messages) + .build(), + ) + + // Append the updated response to final text + aiResponse.content() + .filter { it.isText() } + .forEach { finalText.add(it.text().get().text()) } + } + return finalText.joinToString("\n", prefix = "", postfix = "") } @@ -167,16 +200,27 @@ class MCPClient : AutoCloseable { while (true) { print("\nQuery: ") val message = readlnOrNull() ?: break - if (message.lowercase() == "quit") break - val response = processQuery(message) - println("\n$response") + if (message.trim().lowercase() == "quit") break + + try { + val response = processQuery(message) + println("\n$response") + } catch (e: Exception) { + println("\nError: ${e.message}") + } } } override fun close() { runBlocking { mcp.close() - anthropic.close() } + serverProcess?.let { process -> + process.destroy() + if (!process.waitFor(5, TimeUnit.SECONDS)) { + process.destroyForcibly() + } + } + anthropic.close() } } diff --git a/samples/kotlin-mcp-client/src/main/kotlin/io/modelcontextprotocol/sample/client/main.kt b/samples/kotlin-mcp-client/src/main/kotlin/io/modelcontextprotocol/sample/client/main.kt index 6d76180..001a255 100644 --- a/samples/kotlin-mcp-client/src/main/kotlin/io/modelcontextprotocol/sample/client/main.kt +++ b/samples/kotlin-mcp-client/src/main/kotlin/io/modelcontextprotocol/sample/client/main.kt @@ -3,11 +3,14 @@ package io.modelcontextprotocol.sample.client import kotlinx.coroutines.runBlocking fun main(args: Array) = runBlocking { - if (args.isEmpty()) throw IllegalArgumentException("Usage: java -jar /build/libs/kotlin-mcp-client-0.1.0-all.jar ") - val serverPath = args.first() - val client = MCPClient() + require(args.isNotEmpty()) { "Usage: java -jar " } + + val apiKey = System.getenv("ANTHROPIC_API_KEY") + require(!apiKey.isNullOrBlank()) { "ANTHROPIC_API_KEY environment variable is not set" } + + val client = MCPClient(apiKey) client.use { - client.connectToServer(serverPath) + client.connectToServer(args.first()) client.chatLoop() } -} \ No newline at end of file +} diff --git a/samples/kotlin-mcp-client/src/main/resources/simplelogger.properties b/samples/kotlin-mcp-client/src/main/resources/simplelogger.properties new file mode 100644 index 0000000..0a188c8 --- /dev/null +++ b/samples/kotlin-mcp-client/src/main/resources/simplelogger.properties @@ -0,0 +1,7 @@ +# Level of logging for the ROOT logger: ERROR, WARN, INFO, DEBUG, TRACE (default is INFO) +org.slf4j.simpleLogger.defaultLogLevel=INFO +org.slf4j.simpleLogger.showThreadName=true +org.slf4j.simpleLogger.showDateTime=false + +# Log level for specific packages or classes +org.slf4j.simpleLogger.log.io.modelcontextprotocol=INFO diff --git a/samples/kotlin-mcp-server/README.md b/samples/kotlin-mcp-server/README.md index ee87ab7..82dc5e7 100644 --- a/samples/kotlin-mcp-server/README.md +++ b/samples/kotlin-mcp-server/README.md @@ -1,60 +1,85 @@ -# MCP Kotlin Server Sample +# Kotlin MCP Server -A sample implementation of an MCP (Model Communication Protocol) server in Kotlin that demonstrates different server -configurations and transport methods for both JVM and WASM targets. +A sample MCP server demonstrating multiple transport modes: STDIO, SSE (plain), and SSE (Ktor +plugin). -## Features +> **Note:** The SSE transport modes are provided for backward compatibility. For new projects, +> consider using the [simple-streamable-server](../simple-streamable-server) sample, which uses the +> recommended Streamable HTTP transport. -- Multiple server operation modes: - - Standard I/O server (JVM only) - - SSE (Server-Sent Events) server with plain configuration (JVM, WASM) - - SSE server using Ktor plugin (JVM, WASM) -- Multiplatform support -- Built-in capabilities for: - - Prompts management - - Resources handling - - Tools integration +## Overview -## Getting Started +This sample registers a prompt, a tool, and a resource, then lets you choose how to expose them. +STDIO mode is the default and is best for process-based clients. The two SSE modes show how to +serve MCP over HTTP using either a manual Ktor routing setup or the built-in `mcp { }` Ktor plugin. -### Running the Server +## Prerequisites -You can run the server on the JVM or using Kotlin/WASM on Node.js. +- JDK 17+ +- [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) (optional, for testing) +## Build & Run -#### JVM: +### STDIO mode (default) -To run the server on the JVM (defaults to SSE mode with Ktor plugin on port 3001): - -```bash -./gradlew runJvm +```shell +./gradlew run ``` -#### WASM: +Or explicitly: -To run the server using Kotlin/WASM on Node.js (defaults to SSE mode with Ktor plugin on port 3001): - -```bash -./gradlew wasmJsNodeDevelopmentRun +```shell +./gradlew run --args="--stdio" ``` -### Connecting to the Server +Connect with the MCP Inspector: -For servers on JVM or WASM: -1. Start the server -2. Use the [MCP inspector](https://modelcontextprotocol.io/docs/tools/inspector) to connect to `http://localhost:/sse` +```shell +npx @modelcontextprotocol/inspector --config samples/kotlin-mcp-server/mcp-inspector-config.json --server stdio-server +``` -## Server Capabilities +### SSE with Ktor plugin -- **Prompts**: Supports prompt management with list change notifications -- **Resources**: Includes subscription support and list change notifications -- **Tools**: Supports tool management with list change notifications +```shell +./gradlew run --args="--sse-server-ktor 3002" +``` -## Implementation Details +Connect with the MCP Inspector: -The server is implemented using: -- Ktor for HTTP server functionality -- Kotlin coroutines for asynchronous operations -- SSE for real-time communication -- Standard I/O for command-line interface -- Common Kotlin code shared between JVM and WASM targets +```shell +npx @modelcontextprotocol/inspector --config samples/kotlin-mcp-server/mcp-inspector-config.json --server sse-ktor-server +``` + +### SSE with plain configuration + +> **Known issue:** This mode may not work correctly at this time. + +```shell +./gradlew run --args="--sse-server 3001" +``` + +Connect with the MCP Inspector: + +```shell +npx @modelcontextprotocol/inspector --config samples/kotlin-mcp-server/mcp-inspector-config.json --server sse-server +``` + +## MCP Capabilities + +### Tools + +| Name | Description | +|-------------------|------------------------------------------------------| +| `kotlin-sdk-tool` | A test tool that returns a "Hello, world!" greeting. | + +### Prompts + +| Name | Description | +|--------------------|------------------------------------------------------------------------------------| +| `Kotlin Developer` | Generates a prompt to develop a small Kotlin application for a given project name. | + +### Resources + +| Name | URI | Description | +|--------------|-----------------------|---------------------------------------------------------| +| `Web Search` | `https://search.com/` | A placeholder resource demonstrating resource handling. | diff --git a/samples/kotlin-mcp-server/build.gradle.kts b/samples/kotlin-mcp-server/build.gradle.kts index e85100d..90bb9c9 100644 --- a/samples/kotlin-mcp-server/build.gradle.kts +++ b/samples/kotlin-mcp-server/build.gradle.kts @@ -1,50 +1,39 @@ -@file:OptIn(ExperimentalWasmDsl::class, ExperimentalKotlinGradlePluginApi::class) - -import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi -import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl - plugins { - alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.shadow) + application } group = "org.example" version = "0.1.0" -val jvmMainClass = "Main_jvmKt" +application { + mainClass.set("io.modelcontextprotocol.sample.server.MainKt") +} + +dependencies { + implementation(dependencies.platform(libs.ktor.bom)) + implementation(libs.mcp.kotlin.server) + implementation(libs.ktor.server.cio) + implementation(libs.ktor.server.cors) + implementation(libs.slf4j.simple) + + testImplementation(libs.mcp.kotlin.client) + testImplementation(libs.ktor.client.cio) + testImplementation(kotlin("test")) +} + +tasks.test { + useJUnitPlatform() +} kotlin { jvmToolchain(17) - jvm { - binaries { - executable { - mainClass.set(jvmMainClass) - } - } - val jvmJar by tasks.getting(org.gradle.jvm.tasks.Jar::class) { - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - doFirst { - manifest { - attributes["Main-Class"] = jvmMainClass - } - - from(configurations["jvmRuntimeClasspath"].map { if (it.isDirectory) it else zipTree(it) }) - } - } - } - wasmJs { - nodejs() - binaries.executable() - } - - sourceSets { - commonMain.dependencies { - implementation(libs.mcp.kotlin.server) - implementation(libs.ktor.server.cio) - } - jvmMain.dependencies { - implementation(libs.slf4j.simple) - } - wasmJsMain.dependencies {} + compilerOptions { + javaParameters = true + freeCompilerArgs.addAll( + "-Xdebug", + ) } } diff --git a/samples/kotlin-mcp-server/gradle.properties b/samples/kotlin-mcp-server/gradle.properties new file mode 100644 index 0000000..32cead6 --- /dev/null +++ b/samples/kotlin-mcp-server/gradle.properties @@ -0,0 +1,5 @@ +org.gradle.configuration-cache=true +org.gradle.parallel=true +org.gradle.caching=true + +#mcp.kotlin.overrideVersion=0.0.1-SNAPSHOT diff --git a/samples/kotlin-mcp-server/gradle/libs.versions.toml b/samples/kotlin-mcp-server/gradle/libs.versions.toml new file mode 100644 index 0000000..d46c2f1 --- /dev/null +++ b/samples/kotlin-mcp-server/gradle/libs.versions.toml @@ -0,0 +1,20 @@ +[versions] +kotlin = "2.4.10" +ktor = "3.5.2" +mcp-kotlin = "0.15.0" +slf4j = "2.0.19" +shadow = "9.6.1" + +[libraries] +ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" } +ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio" } +ktor-server-cio = { group = "io.ktor", name = "ktor-server-cio" } +ktor-server-cors = { group = "io.ktor", name = "ktor-server-cors" } +mcp-kotlin-client = { group = "io.modelcontextprotocol", name = "kotlin-sdk-client", version.ref = "mcp-kotlin" } +mcp-kotlin-server = { group = "io.modelcontextprotocol", name = "kotlin-sdk-server", version.ref = "mcp-kotlin" } +slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } + +[plugins] +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } diff --git a/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.jar b/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..eddabd2 Binary files /dev/null and b/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.jar differ diff --git a/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.properties b/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ad7845b --- /dev/null +++ b/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/samples/kotlin-mcp-server/gradlew b/samples/kotlin-mcp-server/gradlew new file mode 100755 index 0000000..249efbb --- /dev/null +++ b/samples/kotlin-mcp-server/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/samples/kotlin-mcp-server/gradlew.bat b/samples/kotlin-mcp-server/gradlew.bat new file mode 100644 index 0000000..8508ef6 --- /dev/null +++ b/samples/kotlin-mcp-server/gradlew.bat @@ -0,0 +1,82 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/samples/kotlin-mcp-server/mcp-inspector-config.json b/samples/kotlin-mcp-server/mcp-inspector-config.json new file mode 100644 index 0000000..e23ef69 --- /dev/null +++ b/samples/kotlin-mcp-server/mcp-inspector-config.json @@ -0,0 +1,25 @@ +{ + "mcpServers": { + "stdio-server": { + "command": "java", + "args": [ + "-Dorg.slf4j.simpleLogger.defaultLogLevel=off", + "-jar", + "./build/libs/kotlin-mcp-server-0.1.0-all.jar" + ], + "env": { + }, + "note": "For SSE connections, add this URL directly in your MCP Client" + }, + "sse-server": { + "type": "sse", + "url": "http://127.0.0.1:3001/sse", + "note": "SSE with plain configuration" + }, + "sse-ktor-server": { + "type": "sse", + "url": "http://127.0.0.1:3002/", + "note": "SSE with Ktor plugin" + } + } +} diff --git a/samples/kotlin-mcp-server/settings.gradle.kts b/samples/kotlin-mcp-server/settings.gradle.kts index cac44ab..5dc44ba 100644 --- a/samples/kotlin-mcp-server/settings.gradle.kts +++ b/samples/kotlin-mcp-server/settings.gradle.kts @@ -1,10 +1,8 @@ rootProject.name = "kotlin-mcp-server" -pluginManagement { - repositories { - mavenCentral() - gradlePluginPortal() - } +plugins { + // Apply the foojay-resolver plugin to allow automatic download of JDKs + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" } dependencyResolutionManagement { @@ -12,9 +10,16 @@ dependencyResolutionManagement { mavenLocal() mavenCentral() } + versionCatalogs { create("libs") { - from(files("../../gradle/libs.versions.toml")) + val mcpKotlinVersion = providers.gradleProperty( + "mcp.kotlin.overrideVersion", + ).orNull + if (mcpKotlinVersion != null) { + logger.lifecycle("Using the override version $mcpKotlinVersion of MCP Kotlin SDK") + version("mcp-kotlin", mcpKotlinVersion) + } } } } diff --git a/samples/kotlin-mcp-server/src/commonMain/kotlin/server.kt b/samples/kotlin-mcp-server/src/commonMain/kotlin/server.kt deleted file mode 100644 index 15e8cb0..0000000 --- a/samples/kotlin-mcp-server/src/commonMain/kotlin/server.kt +++ /dev/null @@ -1,152 +0,0 @@ -package shared - -import io.ktor.http.HttpStatusCode -import io.ktor.server.application.install -import io.ktor.server.cio.CIO -import io.ktor.server.engine.embeddedServer -import io.ktor.server.response.respond -import io.ktor.server.routing.post -import io.ktor.server.routing.routing -import io.ktor.server.sse.SSE -import io.ktor.server.sse.sse -import io.ktor.util.collections.ConcurrentMap -import io.modelcontextprotocol.kotlin.sdk.CallToolResult -import io.modelcontextprotocol.kotlin.sdk.GetPromptResult -import io.modelcontextprotocol.kotlin.sdk.Implementation -import io.modelcontextprotocol.kotlin.sdk.PromptArgument -import io.modelcontextprotocol.kotlin.sdk.PromptMessage -import io.modelcontextprotocol.kotlin.sdk.ReadResourceResult -import io.modelcontextprotocol.kotlin.sdk.Role -import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities -import io.modelcontextprotocol.kotlin.sdk.TextContent -import io.modelcontextprotocol.kotlin.sdk.TextResourceContents -import io.modelcontextprotocol.kotlin.sdk.Tool -import io.modelcontextprotocol.kotlin.sdk.server.Server -import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions -import io.modelcontextprotocol.kotlin.sdk.server.SseServerTransport -import io.modelcontextprotocol.kotlin.sdk.server.mcp - -fun configureServer(): Server { - val server = Server( - Implementation( - name = "mcp-kotlin test server", - version = "0.1.0", - ), - ServerOptions( - capabilities = ServerCapabilities( - prompts = ServerCapabilities.Prompts(listChanged = true), - resources = ServerCapabilities.Resources(subscribe = true, listChanged = true), - tools = ServerCapabilities.Tools(listChanged = true), - ), - ), - ) - - server.addPrompt( - name = "Kotlin Developer", - description = "Develop small kotlin applications", - arguments = listOf( - PromptArgument( - name = "Project Name", - description = "Project name for the new project", - required = true, - ), - ), - ) { request -> - GetPromptResult( - "Description for ${request.name}", - messages = listOf( - PromptMessage( - role = Role.user, - content = TextContent( - "Develop a kotlin project named ${request.arguments?.get("Project Name")}", - ), - ), - ), - ) - } - - // Add a tool - server.addTool( - name = "kotlin-sdk-tool", - description = "A test tool", - inputSchema = Tool.Input(), - ) { request -> - CallToolResult( - content = listOf(TextContent("Hello, world!")), - ) - } - - // Add a resource - server.addResource( - uri = "https://search.com/", - name = "Web Search", - description = "Web search engine", - mimeType = "text/html", - ) { request -> - ReadResourceResult( - contents = listOf( - TextResourceContents("Placeholder content for ${request.uri}", request.uri, "text/html"), - ), - ) - } - - return server -} - -suspend fun runSseMcpServerWithPlainConfiguration(port: Int) { - val servers = ConcurrentMap() - println("Starting sse server on port $port. ") - println("Use inspector to connect to the http://localhost:$port/sse") - - embeddedServer(CIO, host = "0.0.0.0", port = port) { - install(SSE) - routing { - sse("/sse") { - val transport = SseServerTransport("/message", this) - val server = configureServer() - - // For SSE, you can also add prompts/tools/resources if needed: - // server.addTool(...), server.addPrompt(...), server.addResource(...) - - servers[transport.sessionId] = server - - server.onClose { - println("Server closed") - servers.remove(transport.sessionId) - } - - server.connect(transport) - } - post("/message") { - println("Received Message") - val sessionId: String = call.request.queryParameters["sessionId"]!! - val transport = servers[sessionId]?.transport as? SseServerTransport - if (transport == null) { - call.respond(HttpStatusCode.NotFound, "Session not found") - return@post - } - - transport.handlePostMessage(call) - } - } - }.startSuspend(wait = true) -} - -/** - * Starts an SSE (Server Sent Events) MCP server using the Ktor framework and the specified port. - * - * The url can be accessed in the MCP inspector at [http://localhost:$port] - * - * @param port The port number on which the SSE MCP server will listen for client connections. - * @return Unit This method does not return a value. - */ -suspend fun runSseMcpServerUsingKtorPlugin(port: Int) { - println("Starting sse server on port $port") - println("Use inspector to connect to the http://localhost:$port/sse") - - embeddedServer(CIO, host = "0.0.0.0", port = port) { - mcp { - return@mcp configureServer() - } - }.startSuspend(wait = true) -} diff --git a/samples/kotlin-mcp-server/src/jvmMain/kotlin/main.jvm.kt b/samples/kotlin-mcp-server/src/jvmMain/kotlin/main.jvm.kt deleted file mode 100644 index 509aa56..0000000 --- a/samples/kotlin-mcp-server/src/jvmMain/kotlin/main.jvm.kt +++ /dev/null @@ -1,50 +0,0 @@ -import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport -import kotlinx.coroutines.Job -import kotlinx.coroutines.runBlocking -import kotlinx.io.asSink -import kotlinx.io.asSource -import kotlinx.io.buffered -import shared.configureServer -import shared.runSseMcpServerUsingKtorPlugin -import shared.runSseMcpServerWithPlainConfiguration - -/** - * Start sse-server mcp on port 3001. - * - * @param args - * - "--stdio": Runs an MCP server using standard input/output. - * - "--sse-server-ktor ": Runs an SSE MCP server using Ktor plugin (default if no argument is provided). - * - "--sse-server ": Runs an SSE MCP server with a plain configuration. - */ -fun main(args: Array): Unit = runBlocking { - val command = args.firstOrNull() ?: "--sse-server-ktor" - val port = args.getOrNull(1)?.toIntOrNull() ?: 3001 - when (command) { - "--stdio" -> runMcpServerUsingStdio() - "--sse-server-ktor" -> runSseMcpServerUsingKtorPlugin(port) - "--sse-server" -> runSseMcpServerWithPlainConfiguration(port) - else -> { - System.err.println("Unknown command: $command") - } - } -} - -fun runMcpServerUsingStdio() { - // Note: The server will handle listing prompts, tools, and resources automatically. - // The handleListResourceTemplates will return empty as defined in the Server code. - val server = configureServer() - val transport = StdioServerTransport( - inputStream = System.`in`.asSource().buffered(), - outputStream = System.out.asSink().buffered() - ) - - runBlocking { - server.connect(transport) - val done = Job() - server.onClose { - done.complete() - } - done.join() - println("Server closed") - } -} \ No newline at end of file diff --git a/samples/kotlin-mcp-server/src/wasmJsMain/kotlin/main.wasmJs.kt b/samples/kotlin-mcp-server/src/main/kotlin/io/modelcontextprotocol/sample/server/main.kt similarity index 64% rename from samples/kotlin-mcp-server/src/wasmJsMain/kotlin/main.wasmJs.kt rename to samples/kotlin-mcp-server/src/main/kotlin/io/modelcontextprotocol/sample/server/main.kt index 7aa1219..ebe7c0d 100644 --- a/samples/kotlin-mcp-server/src/wasmJsMain/kotlin/main.wasmJs.kt +++ b/samples/kotlin-mcp-server/src/main/kotlin/io/modelcontextprotocol/sample/server/main.kt @@ -1,21 +1,27 @@ -import shared.runSseMcpServerUsingKtorPlugin -import shared.runSseMcpServerWithPlainConfiguration +package io.modelcontextprotocol.sample.server + +import kotlinx.coroutines.runBlocking /** * Start sse-server mcp on port 3001. * * @param args + * - "--stdio": Runs an MCP server using standard input/output. * - "--sse-server-ktor ": Runs an SSE MCP server using Ktor plugin (default if no argument is provided). * - "--sse-server ": Runs an SSE MCP server with a plain configuration. */ -suspend fun main(args: Array) { - val command = args.firstOrNull() ?: "--sse-server-ktor" +fun main(vararg args: String): Unit = runBlocking { + val command = args.firstOrNull() ?: "--stdio" val port = args.getOrNull(1)?.toIntOrNull() ?: 3001 when (command) { + "--stdio" -> runMcpServerUsingStdio() + "--sse-server-ktor" -> runSseMcpServerUsingKtorPlugin(port) + "--sse-server" -> runSseMcpServerWithPlainConfiguration(port) + else -> { error("Unknown command: $command") } } -} \ No newline at end of file +} diff --git a/samples/kotlin-mcp-server/src/main/kotlin/io/modelcontextprotocol/sample/server/server.kt b/samples/kotlin-mcp-server/src/main/kotlin/io/modelcontextprotocol/sample/server/server.kt new file mode 100644 index 0000000..2a85c74 --- /dev/null +++ b/samples/kotlin-mcp-server/src/main/kotlin/io/modelcontextprotocol/sample/server/server.kt @@ -0,0 +1,209 @@ +package io.modelcontextprotocol.sample.server + +import io.ktor.http.HttpMethod +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.Application +import io.ktor.server.application.install +import io.ktor.server.cio.CIO +import io.ktor.server.engine.EmbeddedServer +import io.ktor.server.engine.embeddedServer +import io.ktor.server.plugins.cors.routing.CORS +import io.ktor.server.response.respond +import io.ktor.server.routing.post +import io.ktor.server.routing.routing +import io.ktor.server.sse.SSE +import io.ktor.server.sse.sse +import io.ktor.util.collections.ConcurrentMap +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import io.modelcontextprotocol.kotlin.sdk.server.ServerSession +import io.modelcontextprotocol.kotlin.sdk.server.SseServerTransport +import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport +import io.modelcontextprotocol.kotlin.sdk.server.mcp +import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.PromptArgument +import io.modelcontextprotocol.kotlin.sdk.types.PromptMessage +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult +import io.modelcontextprotocol.kotlin.sdk.types.Role +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents +import kotlinx.coroutines.Job +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.runBlocking +import kotlinx.io.asSink +import kotlinx.io.asSource +import kotlinx.io.buffered + +fun configureServer(): Server { + val server = Server( + Implementation( + name = "mcp-kotlin test server", + version = "0.1.0", + ), + ServerOptions( + capabilities = ServerCapabilities( + prompts = ServerCapabilities.Prompts(listChanged = true), + resources = ServerCapabilities.Resources(subscribe = true, listChanged = true), + tools = ServerCapabilities.Tools(listChanged = true), + ), + ), + ) + + server.addPrompt( + name = "Kotlin Developer", + description = "Develop small kotlin applications", + arguments = listOf( + PromptArgument( + name = "Project Name", + description = "Project name for the new project", + required = true, + ), + ), + ) { request -> + GetPromptResult( + messages = listOf( + PromptMessage( + role = Role.User, + content = TextContent( + "Develop a kotlin project named ${request.arguments?.get("Project Name")}", + ), + ), + ), + description = "Description for ${request.name}", + ) + } + + // Add a tool + server.addTool( + name = "kotlin-sdk-tool", + description = "A test tool", + ) { _ -> + CallToolResult( + content = listOf(TextContent("Hello, world!")), + ) + } + + // Add a resource + server.addResource( + uri = "https://search.com/", + name = "Web Search", + description = "Web search engine", + mimeType = "text/html", + ) { request -> + ReadResourceResult( + contents = listOf( + TextResourceContents("Placeholder content for ${request.uri}", request.uri, "text/html"), + ), + ) + } + + return server +} + +fun runSseMcpServerWithPlainConfiguration(port: Int, wait: Boolean = true): EmbeddedServer<*, *> { + printBanner(port = port, path = "/sse") + val serverSessions = ConcurrentMap() + + val server = configureServer() + + val ktorServer = embeddedServer(CIO, host = "127.0.0.1", port = port) { + installCors() + install(SSE) + routing { + sse("/sse") { + val transport = SseServerTransport("/message", this) + val serverSession = server.createSession(transport) + serverSessions[transport.sessionId] = serverSession + + serverSession.onClose { + println("Server session closed for: ${transport.sessionId}") + serverSessions.remove(transport.sessionId) + } + awaitCancellation() + } + post("/message") { + val sessionId: String? = call.request.queryParameters["sessionId"] + if (sessionId == null) { + call.respond(HttpStatusCode.BadRequest, "Missing sessionId parameter") + return@post + } + + val transport = serverSessions[sessionId]?.transport as? SseServerTransport + if (transport == null) { + call.respond(HttpStatusCode.NotFound, "Session not found") + return@post + } + + transport.handlePostMessage(call) + } + } + }.start(wait = wait) + + return ktorServer +} + +/** + * Starts an SSE (Server-Sent Events) MCP server using the Ktor plugin. + * + * This is the recommended approach for SSE servers as it simplifies configuration. + * The URL can be accessed in the MCP inspector at http://localhost:[port]/sse + * + * @param port The port number on which the SSE MCP server will listen for client connections. + */ +fun runSseMcpServerUsingKtorPlugin(port: Int, wait: Boolean = true): EmbeddedServer<*, *> { + printBanner(port) + + val server = embeddedServer(CIO, host = "127.0.0.1", port = port) { + installCors() + mcp { + return@mcp configureServer() + } + }.start(wait = wait) + return server +} + +private fun printBanner(port: Int, path: String = "") { + if (port == 0) { + println("🎬 Starting SSE server on random port") + } else { + println("🎬 Starting SSE server on ${if (port > 0) "port $port" else "random port"}") + println("🔍 Use MCP inspector to connect to http://localhost:$port$path") + } +} + +private fun Application.installCors() { + install(CORS) { + allowMethod(HttpMethod.Options) + allowMethod(HttpMethod.Get) + allowMethod(HttpMethod.Post) + allowMethod(HttpMethod.Delete) + allowNonSimpleContentTypes = true + anyHost() // @TODO: Don't do this in production if possible. Try to limit it. + } +} + +/** + * Starts an MCP server using Standard I/O transport. + * + * This mode is useful for process-based communication where the server + * communicates via stdin/stdout with a parent process or client. + */ +fun runMcpServerUsingStdio() { + val server = configureServer() + val transport = StdioServerTransport( + inputStream = System.`in`.asSource().buffered(), + outputStream = System.out.asSink().buffered(), + ) + + runBlocking { + server.createSession(transport) + val done = Job() + server.onClose { + done.complete() + } + done.join() + } +} diff --git a/samples/kotlin-mcp-server/src/main/resources/simplelogger.properties b/samples/kotlin-mcp-server/src/main/resources/simplelogger.properties new file mode 100644 index 0000000..ba9c7d8 --- /dev/null +++ b/samples/kotlin-mcp-server/src/main/resources/simplelogger.properties @@ -0,0 +1,10 @@ +# Level of logging for the ROOT logger: ERROR, WARN, INFO, DEBUG, TRACE (default is INFO) +org.slf4j.simpleLogger.defaultLogLevel=INFO +org.slf4j.simpleLogger.showThreadName=true +org.slf4j.simpleLogger.showDateTime=false + +org.slf4j.simpleLogger.logFile=./build/stdout.log + +# Log level for specific packages or classes +org.slf4j.simpleLogger.log.io.modelcontextprotocol=DEBUG +org.slf4j.simpleLogger.log.io.ktor=DEBUG diff --git a/samples/kotlin-mcp-server/src/test/kotlin/McpServerType.kt b/samples/kotlin-mcp-server/src/test/kotlin/McpServerType.kt new file mode 100644 index 0000000..b39dc43 --- /dev/null +++ b/samples/kotlin-mcp-server/src/test/kotlin/McpServerType.kt @@ -0,0 +1,17 @@ +import io.ktor.server.engine.EmbeddedServer +import io.modelcontextprotocol.sample.server.runSseMcpServerUsingKtorPlugin +import io.modelcontextprotocol.sample.server.runSseMcpServerWithPlainConfiguration + +enum class McpServerType( + val sseEndpoint: String, + val serverFactory: (port: Int) -> EmbeddedServer<*, *> +) { + KTOR_PLUGIN( + sseEndpoint = "", + serverFactory = { port -> runSseMcpServerUsingKtorPlugin(port, wait = false) } + ), + PLAIN_CONFIGURATION( + sseEndpoint = "/sse", + serverFactory = { port -> runSseMcpServerWithPlainConfiguration(port, wait = false) } + ) +} diff --git a/samples/kotlin-mcp-server/src/test/kotlin/SseServerIntegrationTest.kt b/samples/kotlin-mcp-server/src/test/kotlin/SseServerIntegrationTest.kt new file mode 100644 index 0000000..2e091ba --- /dev/null +++ b/samples/kotlin-mcp-server/src/test/kotlin/SseServerIntegrationTest.kt @@ -0,0 +1,100 @@ +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents +import kotlinx.coroutines.runBlocking +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +abstract class SseServerIntegrationTestBase { + + abstract val client: Client + + @Test + fun `should list tools`(): Unit = runBlocking { + // when + val listToolsResult = client.listTools() + + // then + assertNull(listToolsResult.meta) + + val tools = listToolsResult.tools + assertEquals(actual = tools.size, expected = 1) + assertEquals(expected = listOf("kotlin-sdk-tool"), actual = tools.map { it.name }) + } + + @Test + fun `should list prompts`(): Unit = runBlocking { + // when + val listPromptsResult = client.listPrompts() + + // then + assertNull(listPromptsResult.meta) + + val prompts = listPromptsResult.prompts + + assertEquals(expected = listOf("Kotlin Developer"), actual = prompts.map { it.name }) + } + + @Test + fun `should list resources`(): Unit = runBlocking { + val listResourcesResult = client.listResources() + + // then + assertNull(listResourcesResult.meta) + val resources = listResourcesResult.resources + + assertEquals(expected = listOf("Web Search"), actual = resources.map { it.name }) + } + + @Test + fun `should get resource`(): Unit = runBlocking { + val testResourceUri = "https://search.com/" + val getResourcesResult = client.readResource( + ReadResourceRequest(ReadResourceRequestParams(uri = testResourceUri)), + ) + + // then + assertEquals(expected = null, actual = getResourcesResult.meta) + val contents = getResourcesResult.contents + assertEquals(expected = 1, actual = contents.size) + assertTrue { + contents.contains( + TextResourceContents("Placeholder content for $testResourceUri", testResourceUri, "text/html"), + ) + } + } + + @Test + fun `should call tool`(): Unit = runBlocking { + // when + val toolResult = client.callTool( + name = "kotlin-sdk-tool", + arguments = emptyMap(), + ) + + // then + assertNotNull(toolResult) + assertNull(toolResult.meta) + val content = toolResult.content.single() + assertIs(content, "Tool result should be a text content") + + assertEquals(expected = "Hello, world!", actual = content.text) + assertEquals(expected = "text", actual = "${content.type}".lowercase()) + } +} + +class SseServerKtorPluginIntegrationTest : SseServerIntegrationTestBase() { + private val testEnvironment = TestEnvironment(McpServerType.KTOR_PLUGIN) + override val client: Client = testEnvironment.client +} + +class SseServerPlainConfigurationIntegrationTest : SseServerIntegrationTestBase() { + private val testEnvironment = TestEnvironment(McpServerType.PLAIN_CONFIGURATION) + override val client: Client = testEnvironment.client +} diff --git a/samples/kotlin-mcp-server/src/test/kotlin/TestEnvironment.kt b/samples/kotlin-mcp-server/src/test/kotlin/TestEnvironment.kt new file mode 100644 index 0000000..71932c3 --- /dev/null +++ b/samples/kotlin-mcp-server/src/test/kotlin/TestEnvironment.kt @@ -0,0 +1,44 @@ +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.sse.SSE +import io.ktor.server.engine.EmbeddedServer +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.mcpSseTransport +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import kotlinx.coroutines.runBlocking +import java.util.concurrent.TimeUnit + +class TestEnvironment(private val serverConfig: McpServerType) { + + val server: EmbeddedServer<*, *> = serverConfig.serverFactory(0) + val client: Client + + init { + client = runBlocking { + val port = server.engine.resolvedConnectors().single().port + initClient(port, serverConfig) + } + + Runtime.getRuntime().addShutdownHook( + Thread { + println("🏁 Shutting down server (${serverConfig.name})") + server.stop(500, 700, TimeUnit.MILLISECONDS) + println("☑️ Shutdown complete") + }, + ) + } + + private suspend fun initClient(port: Int, config: McpServerType): Client { + val client = Client( + Implementation(name = "test-client", version = "0.1.0"), + ) + + val httpClient = HttpClient(CIO) { + install(SSE) + } + + val transport = httpClient.mcpSseTransport("http://127.0.0.1:$port/${config.sseEndpoint}") + client.connect(transport) + return client + } +} diff --git a/samples/kotlin-mcp-server/src/test/resources/simplelogger.properties b/samples/kotlin-mcp-server/src/test/resources/simplelogger.properties new file mode 100644 index 0000000..2012bec --- /dev/null +++ b/samples/kotlin-mcp-server/src/test/resources/simplelogger.properties @@ -0,0 +1,8 @@ +# Level of logging for the ROOT logger: ERROR, WARN, INFO, DEBUG, TRACE (default is INFO) +org.slf4j.simpleLogger.defaultLogLevel=INFO +org.slf4j.simpleLogger.showThreadName=true +org.slf4j.simpleLogger.showDateTime=false + +# Log level for specific packages or classes +org.slf4j.simpleLogger.log.io.modelcontextprotocol=TRACE +org.slf4j.simpleLogger.log.io.ktor=TRACE diff --git a/samples/kotlinlang-mcp-server/Dockerfile b/samples/kotlinlang-mcp-server/Dockerfile new file mode 100644 index 0000000..ada7e56 --- /dev/null +++ b/samples/kotlinlang-mcp-server/Dockerfile @@ -0,0 +1,14 @@ +FROM gradle:jdk21-alpine AS build +WORKDIR /project +COPY gradle/ gradle/ +COPY gradlew settings.gradle.kts build.gradle.kts ./ +COPY src/ src/ +RUN ./gradlew shadowJar --no-daemon + +FROM eclipse-temurin:21-jre-alpine +RUN addgroup -S appgroup && adduser -S appuser -G appgroup +WORKDIR /app +COPY --from=build /project/build/libs/*-all.jar app.jar +USER appuser +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/samples/kotlinlang-mcp-server/README.md b/samples/kotlinlang-mcp-server/README.md new file mode 100644 index 0000000..df0dd3e --- /dev/null +++ b/samples/kotlinlang-mcp-server/README.md @@ -0,0 +1,105 @@ +# Kotlinlang MCP Server + +A Streamable HTTP MCP server that exposes the official +[Kotlin documentation](https://kotlinlang.org/docs/) to LLM clients — full-text search via +[Algolia](https://www.algolia.com/) plus page retrieval in markdown through kotlinlang.org's +`_llms` endpoints. + +## Overview + +This sample demonstrates an MCP server that wraps a real external documentation source. It uses the +recommended Streamable HTTP transport, supports multiple concurrent client sessions, and caches +responses in memory to reduce upstream load. The server is intentionally read-only — it exposes two +tools, no prompts, no resources. + +## Prerequisites + +- JDK 21+ +- Algolia credentials for `kotlinlang.org` (exported as environment variables, see + [Configuration](#configuration)) + +## Build & Run + +```shell +export ALGOLIA_APP_ID=... +export ALGOLIA_API_KEY=... +export ALGOLIA_INDEX_NAME=... + +./gradlew run +``` + +The server starts on `http://localhost:8080/mcp` by default. + +Connect with the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector): + +```shell +npx @modelcontextprotocol/inspector +``` + +In the Inspector UI, select **Streamable HTTP** transport and enter `http://localhost:8080/mcp`. + +## MCP Capabilities + +### Tools + +| Name | Description | +|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------| +| `search_kotlinlang` | Full-text search across Kotlin documentation. Returns up to 5 results filtered to `/docs/` pages. Responses cached for 10 minutes. | +| `get_kotlinlang_page` | Fetches a documentation page as markdown via kotlinlang.org's `_llms` endpoint. Accepts a path relative to `/docs/`. Cached for 1 hour. | + +Example paths accepted by `get_kotlinlang_page`: `coroutines-overview`, +`multiplatform/compose-multiplatform-and-jetpack-compose`. Leading/trailing slashes and an optional +`.html` suffix are normalized before resolution. + +## Configuration + +| Variable | Required | Default | Description | +|----------------------|----------|-----------|--------------------------| +| `ALGOLIA_APP_ID` | yes | — | Algolia application ID | +| `ALGOLIA_API_KEY` | yes | — | Algolia search API key | +| `ALGOLIA_INDEX_NAME` | yes | — | Algolia index name | +| `SERVER_PORT` | no | `8080` | HTTP server port | +| `SERVER_HOST` | no | `0.0.0.0` | HTTP server bind address | + +The server does not provide fallback values for `ALGOLIA_*`. Startup fails if any of those +variables are missing. + +## Docker + +Multi-stage build with Alpine-based images. Runs as a non-root user. + +```shell +docker build -t kotlinlang-mcp-server . + +docker run \ + -p 8080:8080 \ + -e ALGOLIA_APP_ID=... \ + -e ALGOLIA_API_KEY=... \ + -e ALGOLIA_INDEX_NAME=... \ + kotlinlang-mcp-server +``` + +## Connecting an MCP client + +Any MCP client that supports Streamable HTTP transport can connect: + +```json +{ + "mcpServers": { + "kotlinlang": { + "url": "http://localhost:8080/mcp" + } + } +} +``` + +## Limitations + +This sample is intended **for demonstration only**. Before production use, consider: + +- **In-memory cache only** — cached data is lost on server restart. +- **No authentication or authorization** — run behind a trusted proxy or restrict network access. +- **No rate limiting** on outgoing requests to Algolia and kotlinlang.org. +- **Permissive CORS** (`anyHost()`) — restrict to specific origins for any non-local deployment. +- **Streamable HTTP only** — no STDIO transport for local-only usage. +- **Tools only** — no MCP resources or prompts are exposed. diff --git a/samples/kotlinlang-mcp-server/build.gradle.kts b/samples/kotlinlang-mcp-server/build.gradle.kts new file mode 100644 index 0000000..f46f006 --- /dev/null +++ b/samples/kotlinlang-mcp-server/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.shadow) + application +} + +group = "org.example" +version = "0.1.0" + +application { + mainClass.set("org.kotlinlang.mcp.ApplicationKt") +} + +dependencies { + implementation(platform(libs.ktor.bom)) + implementation(libs.mcp.kotlin.server) + implementation(libs.ktor.server.netty) + implementation(libs.ktor.server.sse) + implementation(libs.ktor.server.content.negotiation) + implementation(libs.ktor.server.cors) + implementation(libs.ktor.client.cio) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.serialization.kotlinx.json) + implementation(libs.kotlinx.serialization.json) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.logback.classic) + + testImplementation(kotlin("test")) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.ktor.client.mock) + testImplementation(libs.mcp.kotlin.client) + testImplementation(libs.mcp.kotlin.testing) +} + +tasks.test { + useJUnitPlatform() +} + +kotlin { + jvmToolchain(21) +} diff --git a/samples/kotlinlang-mcp-server/gradle.properties b/samples/kotlinlang-mcp-server/gradle.properties new file mode 100644 index 0000000..24a5976 --- /dev/null +++ b/samples/kotlinlang-mcp-server/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.configuration-cache=true +org.gradle.parallel=true +org.gradle.caching=true + diff --git a/samples/kotlinlang-mcp-server/gradle/libs.versions.toml b/samples/kotlinlang-mcp-server/gradle/libs.versions.toml new file mode 100644 index 0000000..0c11d3b --- /dev/null +++ b/samples/kotlinlang-mcp-server/gradle/libs.versions.toml @@ -0,0 +1,31 @@ +[versions] +kotlin = "2.4.20" +ktor = "3.5.2" +mcp-kotlin = "0.15.0" +serialization = "1.11.0" +coroutines = "1.11.0" +logback = "1.6.3" +shadow = "9.6.1" + +[libraries] +ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" } +ktor-server-netty = { group = "io.ktor", name = "ktor-server-netty" } +ktor-server-sse = { group = "io.ktor", name = "ktor-server-sse" } +ktor-server-content-negotiation = { group = "io.ktor", name = "ktor-server-content-negotiation" } +ktor-server-cors = { group = "io.ktor", name = "ktor-server-cors" } +ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio" } +ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation" } +ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json" } +mcp-kotlin-server = { group = "io.modelcontextprotocol", name = "kotlin-sdk-server", version.ref = "mcp-kotlin" } +mcp-kotlin-client = { group = "io.modelcontextprotocol", name = "kotlin-sdk-client", version.ref = "mcp-kotlin" } +mcp-kotlin-testing = { group = "io.modelcontextprotocol", name = "kotlin-sdk-testing", version.ref = "mcp-kotlin" } +kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" } +kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" } +logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" } +kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" } +ktor-client-mock = { group = "io.ktor", name = "ktor-client-mock" } + +[plugins] +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } diff --git a/samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.jar b/samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..eddabd2 Binary files /dev/null and b/samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.jar differ diff --git a/samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.properties b/samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ad7845b --- /dev/null +++ b/samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/samples/kotlinlang-mcp-server/gradlew b/samples/kotlinlang-mcp-server/gradlew new file mode 100755 index 0000000..249efbb --- /dev/null +++ b/samples/kotlinlang-mcp-server/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/samples/kotlinlang-mcp-server/gradlew.bat b/samples/kotlinlang-mcp-server/gradlew.bat new file mode 100644 index 0000000..8508ef6 --- /dev/null +++ b/samples/kotlinlang-mcp-server/gradlew.bat @@ -0,0 +1,82 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/samples/kotlinlang-mcp-server/settings.gradle.kts b/samples/kotlinlang-mcp-server/settings.gradle.kts new file mode 100644 index 0000000..bf867e0 --- /dev/null +++ b/samples/kotlinlang-mcp-server/settings.gradle.kts @@ -0,0 +1,25 @@ +rootProject.name = "kotlinlang-mcp-server" + +plugins { + // Apply the foojay-resolver plugin to allow automatic download of JDKs + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} + +dependencyResolutionManagement { + repositories { + mavenLocal() + mavenCentral() + } + + versionCatalogs { + create("libs") { + val mcpKotlinVersion = providers.gradleProperty( + "mcp.kotlin.overrideVersion", + ).orNull + if (mcpKotlinVersion != null) { + logger.lifecycle("Using the override version $mcpKotlinVersion of MCP Kotlin SDK") + version("mcp-kotlin", mcpKotlinVersion) + } + } + } +} diff --git a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/Application.kt b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/Application.kt new file mode 100644 index 0000000..e50b1ad --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/Application.kt @@ -0,0 +1,28 @@ +package org.kotlinlang.mcp + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.netty.* +import io.ktor.server.plugins.cors.routing.* +import io.modelcontextprotocol.kotlin.sdk.server.mcpStreamableHttp +import org.kotlinlang.mcp.config.toServerConfig + +fun main(args: Array) = EngineMain.main(args) + +fun Application.module() { + val config = environment.config.toServerConfig() + val kotlinlangServer = KotlinlangServer(config) + + monitor.subscribe(ApplicationStopped) { + kotlinlangServer.close() + } + + install(CORS) { + anyHost() + allowHeader(HttpHeaders.ContentType) + } + + mcpStreamableHttp { + kotlinlangServer.server + } +} diff --git a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/KotlinlangServer.kt b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/KotlinlangServer.kt new file mode 100644 index 0000000..0814342 --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/KotlinlangServer.kt @@ -0,0 +1,118 @@ +package org.kotlinlang.mcp + +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.serialization.kotlinx.json.* +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import io.modelcontextprotocol.kotlin.sdk.types.* +import kotlinx.serialization.json.* +import org.kotlinlang.mcp.algolia.AlgoliaClient +import org.kotlinlang.mcp.cache.TtlCache +import org.kotlinlang.mcp.config.ServerConfig +import org.kotlinlang.mcp.content.PageFetcher +import org.kotlinlang.mcp.tools.GetKotlinlangPage +import org.kotlinlang.mcp.tools.SearchKotlinlang +import org.slf4j.LoggerFactory +import java.io.Closeable +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.minutes + +internal class KotlinlangServer(config: ServerConfig) : Closeable { + + private val logger = LoggerFactory.getLogger(KotlinlangServer::class.java) + + private val httpClient = HttpClient(CIO) { + install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) } + install(HttpTimeout) { requestTimeoutMillis = 10_000 } + expectSuccess = true + } + + private val searchCache = TtlCache(10.minutes) + private val pageCache = TtlCache(1.hours) + + private val algoliaClient = AlgoliaClient(config, httpClient) + private val pageFetcher = PageFetcher(httpClient) + private val searchTool = SearchKotlinlang(algoliaClient, searchCache) + private val pageTool = GetKotlinlangPage(pageFetcher, pageCache) + + val server: Server = Server( + serverInfo = Implementation(name = "kotlinlang-mcp-server", version = "1.0.0"), + options = ServerOptions( + capabilities = ServerCapabilities( + tools = ServerCapabilities.Tools(listChanged = false), + ), + ), + ) { + addTool( + name = "search_kotlinlang", + description = "Search across Kotlin documentation on kotlinlang.org to find relevant pages, " + + "code examples, API references, and guides. Use this tool when you need to answer questions " + + "about Kotlin language features, standard library, coroutines, multiplatform, tooling, or any " + + "other topic covered in the official Kotlin documentation. Returns up to 5 results with page " + + "titles, paths, and text snippets. To get the full content of a specific page, use the " + + "get_kotlinlang_page tool with the page path from the search results.", + inputSchema = ToolSchema( + properties = buildJsonObject { + putJsonObject("query") { + put("type", "string") + put( + "description", + "A search query to find relevant Kotlin documentation pages " + + "(e.g. 'coroutines', 'sealed classes', 'multiplatform setup')", + ) + } + }, + required = listOf("query"), + ), + toolAnnotations = ToolAnnotations(readOnlyHint = true, openWorldHint = true), + ) { request -> + val query = request.arguments?.get("query")?.jsonPrimitive?.content + ?: return@addTool CallToolResult( + content = listOf(TextContent(text = "Missing required argument: query")), + isError = true, + ) + logger.info("search_kotlinlang query=\"{}\"", query) + searchTool.handle(query) + } + + addTool( + name = "get_kotlinlang_page", + description = "Retrieve the full content of a specific Kotlin documentation page from " + + "kotlinlang.org in md format. Use this tool when you already know the " + + "page path (e.g., from search results) and need the complete content of that page rather " + + "than just a snippet. If the page is not found, use search_kotlinlang to discover the " + + "correct path.", + inputSchema = ToolSchema( + properties = buildJsonObject { + putJsonObject("path") { + put("type", "string") + put( + "description", + "Page path relative to /docs/, without extension. Use the page paths " + + "returned from search_kotlinlang results " + + "(e.g. 'coroutines-overview', " + + "'multiplatform/compose-multiplatform-and-jetpack-compose')", + ) + } + }, + required = listOf("path"), + ), + toolAnnotations = ToolAnnotations(readOnlyHint = true, openWorldHint = true), + ) { request -> + val path = request.arguments?.get("path")?.jsonPrimitive?.content + ?: return@addTool CallToolResult( + content = listOf(TextContent(text = "Missing required argument: path")), + isError = true, + ) + logger.info("get_kotlinlang_page path=\"{}\"", path) + pageTool.handle(path) + } + } + + override fun close() { + httpClient.close() + } +} diff --git a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/algolia/AlgoliaClient.kt b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/algolia/AlgoliaClient.kt new file mode 100644 index 0000000..0d0db42 --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/algolia/AlgoliaClient.kt @@ -0,0 +1,38 @@ +package org.kotlinlang.mcp.algolia + +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.request.* +import io.ktor.http.* +import org.kotlinlang.mcp.config.ServerConfig + +internal class AlgoliaClient( + private val config: ServerConfig, + private val httpClient: HttpClient, +) { + suspend fun search(query: String): AlgoliaSearchResponse { + val response = httpClient.post(buildSearchUrl()) { + header("x-algolia-application-id", config.algoliaAppId) + header("x-algolia-api-key", config.algoliaApiKey) + contentType(ContentType.Application.Json) + setBody( + AlgoliaSearchRequest( + query = query, + hitsPerPage = HITS_PER_PAGE, + attributesToRetrieve = ATTRIBUTES_TO_RETRIEVE, + attributesToSnippet = ATTRIBUTES_TO_SNIPPET, + ) + ) + } + return response.body() + } + + private fun buildSearchUrl(): String = + "https://${config.algoliaAppId}-dsn.algolia.net/1/indexes/${config.algoliaIndexName}/query" + + companion object { + private const val HITS_PER_PAGE = 15 + private val ATTRIBUTES_TO_RETRIEVE = listOf("objectID", "mainTitle", "url", "headings") + private val ATTRIBUTES_TO_SNIPPET = listOf("content:40") + } +} diff --git a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/algolia/AlgoliaModels.kt b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/algolia/AlgoliaModels.kt new file mode 100644 index 0000000..88c465d --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/algolia/AlgoliaModels.kt @@ -0,0 +1,38 @@ +package org.kotlinlang.mcp.algolia + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal data class AlgoliaSearchRequest( + val query: String, + val hitsPerPage: Int, + val attributesToRetrieve: List, + val attributesToSnippet: List, +) + +@Serializable +internal data class AlgoliaSearchResponse( + val hits: List, +) + +@Serializable +internal data class AlgoliaHit( + val objectID: String, + val mainTitle: String? = null, + val url: String, + val headings: String? = null, + @SerialName("_snippetResult") + val snippetResult: SnippetResult? = null, +) + +@Serializable +internal data class SnippetResult( + val content: SnippetValue, +) + +@Serializable +internal data class SnippetValue( + val value: String, + val matchLevel: String, +) diff --git a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/cache/TtlCache.kt b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/cache/TtlCache.kt new file mode 100644 index 0000000..7ba8de1 --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/cache/TtlCache.kt @@ -0,0 +1,71 @@ +package org.kotlinlang.mcp.cache + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive +import java.util.concurrent.CancellationException +import java.util.concurrent.ConcurrentHashMap +import kotlin.time.Duration +import kotlin.time.TimeMark +import kotlin.time.TimeSource + +internal class TtlCache( + private val ttl: Duration, + private val timeSource: TimeSource = TimeSource.Monotonic, +) { + private class Entry(val value: V, val createdAt: TimeMark) + + private val map = ConcurrentHashMap>() + private val inFlight = ConcurrentHashMap>() + + fun get(key: K): V? { + val entry = map[key] ?: return null + if (entry.createdAt.elapsedNow() >= ttl) { + map.remove(key, entry) + return null + } + return entry.value + } + + fun put(key: K, value: V) { + map[key] = Entry(value, timeSource.markNow()) + evictExpired() + } + + private fun evictExpired() { + map.entries.removeIf { it.value.createdAt.elapsedNow() >= ttl } + } + + suspend fun getOrPut(key: K, loader: suspend () -> V): V { + while (true) { + get(key)?.let { return it } + + val deferred = CompletableDeferred() + val existing = inFlight.putIfAbsent(key, deferred) + if (existing != null) { + try { + return existing.await() + } catch (_: CancellationException) { + currentCoroutineContext().ensureActive() + // The loader's scope was cancelled, not ours — retry + continue + } + } + + try { + val value = loader() + put(key, value) + deferred.complete(value) + return value + } catch (e: CancellationException) { + deferred.cancel() + throw e + } catch (e: Throwable) { + deferred.completeExceptionally(e) + throw e + } finally { + inFlight.remove(key, deferred) + } + } + } +} diff --git a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/config/ServerConfig.kt b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/config/ServerConfig.kt new file mode 100644 index 0000000..0c61e13 --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/config/ServerConfig.kt @@ -0,0 +1,18 @@ +package org.kotlinlang.mcp.config + +import io.ktor.server.config.* + +internal data class ServerConfig( + val algoliaAppId: String, + val algoliaApiKey: String, + val algoliaIndexName: String, +) { + override fun toString(): String = + "ServerConfig(algoliaAppId=$algoliaAppId, algoliaIndexName=$algoliaIndexName)" +} + +internal fun ApplicationConfig.toServerConfig(): ServerConfig = ServerConfig( + algoliaAppId = property("algolia.appId").getString(), + algoliaApiKey = property("algolia.apiKey").getString(), + algoliaIndexName = property("algolia.indexName").getString(), +) diff --git a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/content/PageFetcher.kt b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/content/PageFetcher.kt new file mode 100644 index 0000000..a8495b2 --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/content/PageFetcher.kt @@ -0,0 +1,11 @@ +package org.kotlinlang.mcp.content + +import io.ktor.client.* +import io.ktor.client.request.* +import io.ktor.client.statement.* + +internal class PageFetcher(private val httpClient: HttpClient) { + suspend fun fetch(url: String): String { + return httpClient.get(url).bodyAsText() + } +} diff --git a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/content/UrlMapper.kt b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/content/UrlMapper.kt new file mode 100644 index 0000000..ab0a8ff --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/content/UrlMapper.kt @@ -0,0 +1,20 @@ +package org.kotlinlang.mcp.content + +private const val BASE_URL = "https://kotlinlang.org/docs" + +internal fun normalizePath(path: String): String = path.trim().trim('/').removeSuffix(".html") + +internal fun mapPathToUrl(path: String): String { + val normalized = normalizePath(path) + require(normalized.isNotBlank()) { "Path must not be empty" } + require(".." !in normalized.split('/')) { "Path must not contain '..' segments" } + + val lastSlash = normalized.lastIndexOf('/') + return if (lastSlash == -1) { + "$BASE_URL/_llms/$normalized.txt" + } else { + val dir = normalized.substring(0, lastSlash) + val file = normalized.substring(lastSlash + 1) + "$BASE_URL/$dir/_llms/$file.txt" + } +} diff --git a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/tools/GetKotlinlangPage.kt b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/tools/GetKotlinlangPage.kt new file mode 100644 index 0000000..d4a5097 --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/tools/GetKotlinlangPage.kt @@ -0,0 +1,55 @@ +package org.kotlinlang.mcp.tools + +import io.ktor.client.plugins.* +import io.ktor.http.* +import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import kotlinx.coroutines.CancellationException +import org.kotlinlang.mcp.cache.TtlCache +import org.kotlinlang.mcp.content.PageFetcher +import org.kotlinlang.mcp.content.mapPathToUrl +import org.kotlinlang.mcp.content.normalizePath +import org.slf4j.LoggerFactory + +internal class GetKotlinlangPage( + private val pageFetcher: PageFetcher, + private val cache: TtlCache, +) { + private val logger = LoggerFactory.getLogger(GetKotlinlangPage::class.java) + suspend fun handle(path: String): CallToolResult { + val normalizedPath = normalizePath(path) + return try { + val content = cache.getOrPut(normalizedPath) { + val url = mapPathToUrl(normalizedPath) + pageFetcher.fetch(url) + } + CallToolResult(content = listOf(TextContent(text = content))) + } catch (e: CancellationException) { + throw e + } catch (e: IllegalArgumentException) { + CallToolResult( + content = listOf(TextContent(text = "Invalid path: ${e.message}")), + isError = true, + ) + } catch (e: ClientRequestException) { + if (e.response.status != HttpStatusCode.NotFound) { + logger.error("Failed to fetch page path=\"{}\"", path, e) + } + val message = if (e.response.status == HttpStatusCode.NotFound) { + "Page not found: $path. Use search_kotlinlang to find the correct page path." + } else { + "Failed to fetch page: ${e.message}" + } + CallToolResult( + content = listOf(TextContent(text = message)), + isError = true, + ) + } catch (e: Exception) { + logger.error("Failed to fetch page path=\"{}\"", path, e) + CallToolResult( + content = listOf(TextContent(text = "Failed to fetch page: ${e.message ?: "unknown error"}")), + isError = true, + ) + } + } +} diff --git a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/tools/SearchKotlinlang.kt b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/tools/SearchKotlinlang.kt new file mode 100644 index 0000000..886f3f7 --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/tools/SearchKotlinlang.kt @@ -0,0 +1,62 @@ +package org.kotlinlang.mcp.tools + +import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import kotlinx.coroutines.CancellationException +import org.kotlinlang.mcp.algolia.AlgoliaClient +import org.kotlinlang.mcp.algolia.AlgoliaHit +import org.kotlinlang.mcp.cache.TtlCache +import org.slf4j.LoggerFactory + +internal class SearchKotlinlang( + private val algoliaClient: AlgoliaClient, + private val cache: TtlCache, +) { + private val logger = LoggerFactory.getLogger(SearchKotlinlang::class.java) + + companion object { + private const val MAX_RESULTS = 5 + private val HTML_TAG_REGEX = Regex("<[^>]+>") + } + + suspend fun handle(query: String): CallToolResult { + return try { + val text = cache.getOrPut(query) { + val response = algoliaClient.search(query) + val filtered = response.hits + .filter { it.url.startsWith("/docs/") } + .take(MAX_RESULTS) + if (filtered.isEmpty()) { + "No results found for: $query" + } else { + formatResults(filtered) + } + } + CallToolResult(content = listOf(TextContent(text = text))) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + logger.error("Search failed for query=\"{}\"", query, e) + CallToolResult( + content = listOf(TextContent(text = "Search failed: ${e.message ?: "unknown error"}")), + isError = true, + ) + } + } + + private fun formatResults(hits: List): String = + hits.mapIndexed { index, hit -> + val title = hit.mainTitle ?: "Untitled" + val path = extractPath(hit.url) + val snippetLine = hit.snippetResult?.content?.value + ?.let { "\n ${stripHtml(it)}" } + ?: "" + "${index + 1}. $title [$path]$snippetLine" + }.joinToString("\n\n") + + private fun extractPath(url: String): String = + url.removePrefix("/docs/").removeSuffix(".html") + + private fun stripHtml(text: String): String = + text.replace(HTML_TAG_REGEX, "") +} diff --git a/samples/kotlinlang-mcp-server/src/main/resources/application.conf b/samples/kotlinlang-mcp-server/src/main/resources/application.conf new file mode 100644 index 0000000..4908da5 --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/main/resources/application.conf @@ -0,0 +1,17 @@ +ktor { + deployment { + port = 8080 + port = ${?SERVER_PORT} + host = "0.0.0.0" + host = ${?SERVER_HOST} + } + application { + modules = [ org.kotlinlang.mcp.ApplicationKt.module ] + } +} + +algolia { + appId = ${ALGOLIA_APP_ID} + apiKey = ${ALGOLIA_API_KEY} + indexName = ${ALGOLIA_INDEX_NAME} +} diff --git a/samples/kotlinlang-mcp-server/src/main/resources/logback.xml b/samples/kotlinlang-mcp-server/src/main/resources/logback.xml new file mode 100644 index 0000000..0ffe8a3 --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/main/resources/logback.xml @@ -0,0 +1,15 @@ + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + diff --git a/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/KotlinlangServerTest.kt b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/KotlinlangServerTest.kt new file mode 100644 index 0000000..c6786fb --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/KotlinlangServerTest.kt @@ -0,0 +1,62 @@ +package org.kotlinlang.mcp + +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions +import io.modelcontextprotocol.kotlin.sdk.testing.ChannelTransport +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import org.kotlinlang.mcp.config.ServerConfig +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +@OptIn(ExperimentalMcpApi::class) +class KotlinlangServerTest { + + private val testConfig = ServerConfig( + algoliaAppId = "test-app-id", + algoliaApiKey = "test-api-key", + algoliaIndexName = "test-index", + ) + + @Test + fun `server registers both tools with correct schema and annotations`() = runTest { + val kotlinlangServer = KotlinlangServer(testConfig) + kotlinlangServer.use { + val (clientTransport, serverTransport) = ChannelTransport.createLinkedPair() + + val client = Client( + clientInfo = Implementation(name = "test-client", version = "1.0"), + options = ClientOptions(), + ) + + joinAll( + launch { client.connect(clientTransport) }, + launch { kotlinlangServer.server.createSession(serverTransport) }, + ) + + val tools = client.listTools().tools + + assertEquals(2, tools.size) + + val searchTool = tools.find { it.name == "search_kotlinlang" } + val searchProps = searchTool?.inputSchema?.properties + assertTrue(searchProps?.containsKey("query") ?: false) + assertEquals(listOf("query"), searchTool.inputSchema.required) + assertEquals(true, searchTool.annotations?.readOnlyHint) + assertEquals(true, searchTool.annotations?.openWorldHint) + + val pageTool = tools.find { it.name == "get_kotlinlang_page" } + val pageProps = pageTool?.inputSchema?.properties + assertTrue(pageProps?.containsKey("path") ?: false) + assertEquals(listOf("path"), pageTool.inputSchema.required) + assertEquals(true, pageTool.annotations?.readOnlyHint) + assertEquals(true, pageTool.annotations?.openWorldHint) + + client.close() + } + } +} diff --git a/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/algolia/AlgoliaClientTest.kt b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/algolia/AlgoliaClientTest.kt new file mode 100644 index 0000000..4f9dff6 --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/algolia/AlgoliaClientTest.kt @@ -0,0 +1,166 @@ +package org.kotlinlang.mcp.algolia + +import io.ktor.client.* +import io.ktor.client.engine.mock.* +import io.ktor.client.plugins.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json +import org.kotlinlang.mcp.config.ServerConfig +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +class AlgoliaClientTest { + + private val testConfig = ServerConfig( + algoliaAppId = "test-app-id", + algoliaApiKey = "test-api-key", + algoliaIndexName = "test-index", + ) + + private val json = Json { ignoreUnknownKeys = true } + + private fun createClient(mockEngine: MockEngine): AlgoliaClient { + val httpClient = HttpClient(mockEngine) { + install(ContentNegotiation) { json(json) } + expectSuccess = true + } + return AlgoliaClient(testConfig, httpClient) + } + + @Test + fun `search sends correct URL and headers`() = runTest { + val mockEngine = MockEngine { request -> + assertEquals( + "https://test-app-id-dsn.algolia.net/1/indexes/test-index/query", + request.url.toString(), + ) + assertEquals("test-app-id", request.headers["x-algolia-application-id"]) + assertEquals("test-api-key", request.headers["x-algolia-api-key"]) + assertEquals(HttpMethod.Post, request.method) + + respond( + content = """{"hits": []}""", + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json"), + ) + } + + val client = createClient(mockEngine) + client.search("coroutines") + } + + @Test + fun `search sends correct request body`() = runTest { + val mockEngine = MockEngine { request -> + val body = json.decodeFromString(request.body.toByteArray().decodeToString()) + assertEquals("coroutines", body.query) + assertEquals(15, body.hitsPerPage) + assertEquals(listOf("objectID", "mainTitle", "url", "headings"), body.attributesToRetrieve) + assertEquals(listOf("content:40"), body.attributesToSnippet) + + respond( + content = """{"hits": []}""", + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json"), + ) + } + + val client = createClient(mockEngine) + client.search("coroutines") + } + + @Test + fun `search parses response with hits`() = runTest { + val responseJson = """ + { + "hits": [ + { + "objectID": "abc123", + "mainTitle": "Coroutines overview", + "url": "/docs/coroutines-overview.html", + "headings": "Introduction | First coroutine", + "_snippetResult": { + "content": { + "value": "Kotlin provides coroutines support", + "matchLevel": "full" + } + } + } + ] + } + """.trimIndent() + + val mockEngine = MockEngine { + respond( + content = responseJson, + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json"), + ) + } + + val client = createClient(mockEngine) + val result = client.search("coroutines") + + assertEquals(1, result.hits.size) + val hit = result.hits[0] + assertEquals("abc123", hit.objectID) + assertEquals("Coroutines overview", hit.mainTitle) + assertEquals("/docs/coroutines-overview.html", hit.url) + assertEquals("Introduction | First coroutine", hit.headings) + assertEquals("Kotlin provides coroutines support", hit.snippetResult?.content?.value) + assertEquals("full", hit.snippetResult?.content?.matchLevel) + } + + @Test + fun `search returns empty list when no hits`() = runTest { + val mockEngine = MockEngine { + respond( + content = """{"hits": []}""", + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json"), + ) + } + + val client = createClient(mockEngine) + val result = client.search("nonexistent") + + assertTrue(result.hits.isEmpty()) + } + + @Test + fun `search throws on 4xx error`() = runTest { + val mockEngine = MockEngine { + respond( + content = """{"message": "Invalid Application-ID or API key"}""", + status = HttpStatusCode.Forbidden, + headers = headersOf(HttpHeaders.ContentType, "application/json"), + ) + } + + val client = createClient(mockEngine) + assertFailsWith { + client.search("coroutines") + } + } + + @Test + fun `search throws on 5xx error`() = runTest { + val mockEngine = MockEngine { + respond( + content = """{"message": "Internal server error"}""", + status = HttpStatusCode.InternalServerError, + headers = headersOf(HttpHeaders.ContentType, "application/json"), + ) + } + + val client = createClient(mockEngine) + assertFailsWith { + client.search("coroutines") + } + } +} diff --git a/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/algolia/AlgoliaModelsTest.kt b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/algolia/AlgoliaModelsTest.kt new file mode 100644 index 0000000..10dbe5b --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/algolia/AlgoliaModelsTest.kt @@ -0,0 +1,126 @@ +package org.kotlinlang.mcp.algolia + +import kotlinx.serialization.json.Json +import kotlin.test.Test +import kotlin.test.assertEquals + +class AlgoliaModelsTest { + + private val json = Json { ignoreUnknownKeys = true } + + @Test + fun `serializes search request to JSON`() { + val request = AlgoliaSearchRequest( + query = "coroutines", + hitsPerPage = 15, + attributesToRetrieve = listOf("mainTitle", "url", "headings"), + attributesToSnippet = listOf("content:40"), + ) + + val encoded = json.encodeToString(request) + val decoded = json.decodeFromString(encoded) + + assertEquals(request, decoded) + } + + @Test + fun `parses full response with nested snippet`() { + val responseJson = """ + { + "hits": [ + { + "objectID": "abc123", + "mainTitle": "Coroutines overview", + "url": "/docs/coroutines-overview.html", + "headings": "Introduction | First coroutine", + "_snippetResult": { + "content": { + "value": "Kotlin provides coroutines support", + "matchLevel": "full" + } + } + } + ] + } + """.trimIndent() + + val response = json.decodeFromString(responseJson) + + assertEquals(1, response.hits.size) + val hit = response.hits[0] + assertEquals("abc123", hit.objectID) + assertEquals("Coroutines overview", hit.mainTitle) + assertEquals("/docs/coroutines-overview.html", hit.url) + assertEquals("Introduction | First coroutine", hit.headings) + assertEquals("Kotlin provides coroutines support", hit.snippetResult?.content?.value) + assertEquals("full", hit.snippetResult?.content?.matchLevel) + } + + @Test + fun `parses hit with missing nullable fields`() { + val responseJson = """ + { + "hits": [ + { + "objectID": "def456", + "url": "/docs/basic-syntax.html" + } + ] + } + """.trimIndent() + + val response = json.decodeFromString(responseJson) + + val hit = response.hits[0] + assertEquals("def456", hit.objectID) + assertEquals("/docs/basic-syntax.html", hit.url) + assertEquals(null, hit.mainTitle) + assertEquals(null, hit.headings) + assertEquals(null, hit.snippetResult) + } + + @Test + fun `parses response ignoring unknown fields`() { + val responseJson = """ + { + "hits": [ + { + "objectID": "ghi789", + "url": "/docs/functions.html", + "unknownField": 42, + "_snippetResult": { + "content": { + "value": "snippet text", + "matchLevel": "none", + "fullyHighlighted": false + }, + "anotherUnknown": "ignored" + } + } + ], + "nbHits": 100, + "page": 0, + "processingTimeMS": 5 + } + """.trimIndent() + + val response = json.decodeFromString(responseJson) + + assertEquals(1, response.hits.size) + assertEquals("ghi789", response.hits[0].objectID) + assertEquals("snippet text", response.hits[0].snippetResult?.content?.value) + } + + @Test + fun `parses response with empty hits list`() { + val responseJson = """ + { + "hits": [] + } + """.trimIndent() + + val response = json.decodeFromString(responseJson) + + assertEquals(emptyList(), response.hits) + } +} diff --git a/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/cache/TtlCacheTest.kt b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/cache/TtlCacheTest.kt new file mode 100644 index 0000000..a191fe7 --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/cache/TtlCacheTest.kt @@ -0,0 +1,144 @@ +package org.kotlinlang.mcp.cache + +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.time.Duration.Companion.minutes +import kotlin.time.TestTimeSource + +class TtlCacheTest { + + @Test + fun `get returns value that was put`() { + val cache = TtlCache(ttl = 10.minutes) + cache.put("key", "value") + assertEquals("value", cache.get("key")) + } + + @Test + fun `get returns null for missing key`() { + val cache = TtlCache(ttl = 10.minutes) + assertNull(cache.get("missing")) + } + + @Test + fun `get returns null after TTL expires`() { + val timeSource = TestTimeSource() + val cache = TtlCache(ttl = 10.minutes, timeSource = timeSource) + + cache.put("key", "value") + assertEquals("value", cache.get("key")) + + timeSource += 11.minutes + assertNull(cache.get("key")) + } + + @Test + fun `get returns value just before TTL expires`() { + val timeSource = TestTimeSource() + val cache = TtlCache(ttl = 10.minutes, timeSource = timeSource) + + cache.put("key", "value") + timeSource += 9.minutes + assertEquals("value", cache.get("key")) + } + + @Test + fun `put overwrites value and resets TTL`() { + val timeSource = TestTimeSource() + val cache = TtlCache(ttl = 10.minutes, timeSource = timeSource) + + cache.put("key", "old") + timeSource += 8.minutes + + cache.put("key", "new") + timeSource += 8.minutes + + assertEquals("new", cache.get("key")) + } + + @Test + fun `getOrPut returns cached value without calling loader`() = runTest { + val cache = TtlCache(ttl = 10.minutes) + cache.put("key", "cached") + + var loaderCalled = false + val result = cache.getOrPut("key") { + loaderCalled = true + "loaded" + } + + assertEquals("cached", result) + assertEquals(false, loaderCalled) + } + + @Test + fun `getOrPut calls loader on cache miss and caches result`() = runTest { + val cache = TtlCache(ttl = 10.minutes) + + val result = cache.getOrPut("key") { "loaded" } + + assertEquals("loaded", result) + assertEquals("loaded", cache.get("key")) + } + + @Test + fun `getOrPut calls loader again after TTL expires`() = runTest { + val timeSource = TestTimeSource() + val cache = TtlCache(ttl = 10.minutes, timeSource = timeSource) + + cache.getOrPut("key") { "first" } + timeSource += 11.minutes + + val result = cache.getOrPut("key") { "second" } + assertEquals("second", result) + } + + @Test + fun `put evicts expired entries`() { + val timeSource = TestTimeSource() + val cache = TtlCache(ttl = 10.minutes, timeSource = timeSource) + + cache.put("a", "1") + cache.put("b", "2") + timeSource += 11.minutes + + // Both entries are expired but still in memory. + // A new put triggers eviction of expired entries. + cache.put("c", "3") + + assertNull(cache.get("a")) + assertNull(cache.get("b")) + assertEquals("3", cache.get("c")) + } + + @Test + fun `put does not evict non-expired entries`() { + val timeSource = TestTimeSource() + val cache = TtlCache(ttl = 10.minutes, timeSource = timeSource) + + cache.put("a", "1") + timeSource += 5.minutes + cache.put("b", "2") + + assertEquals("1", cache.get("a")) + assertEquals("2", cache.get("b")) + } + + @Test + fun `concurrent put and get do not throw`() = runTest { + val cache = TtlCache(ttl = 10.minutes) + val jobs = (1..100).map { i -> + launch { + cache.put(i, "value-$i") + cache.get(i) + cache.put(i, "updated-$i") + cache.get(i) + } + } + jobs.joinAll() + } +} diff --git a/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/config/ServerConfigTest.kt b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/config/ServerConfigTest.kt new file mode 100644 index 0000000..5714602 --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/config/ServerConfigTest.kt @@ -0,0 +1,35 @@ +package org.kotlinlang.mcp.config + +import io.ktor.server.config.* +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class ServerConfigTest { + + @Test + fun `toServerConfig maps all Algolia properties`() { + val config = MapApplicationConfig( + "algolia.appId" to "test-app-id", + "algolia.apiKey" to "test-api-key", + "algolia.indexName" to "test-index", + ) + + val serverConfig = config.toServerConfig() + + assertEquals("test-app-id", serverConfig.algoliaAppId) + assertEquals("test-api-key", serverConfig.algoliaApiKey) + assertEquals("test-index", serverConfig.algoliaIndexName) + } + + @Test + fun `toServerConfig throws when required property is missing`() { + val config = MapApplicationConfig( + "algolia.appId" to "test-app-id", + ) + + assertFailsWith { + config.toServerConfig() + } + } +} diff --git a/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/content/PageFetcherTest.kt b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/content/PageFetcherTest.kt new file mode 100644 index 0000000..1cd3e6b --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/content/PageFetcherTest.kt @@ -0,0 +1,87 @@ +package org.kotlinlang.mcp.content + +import io.ktor.client.* +import io.ktor.client.engine.mock.* +import io.ktor.client.plugins.* +import io.ktor.http.* +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class PageFetcherTest { + + private fun createFetcher(mockEngine: MockEngine): PageFetcher { + val httpClient = HttpClient(mockEngine) { + expectSuccess = true + } + return PageFetcher(httpClient) + } + + @Test + fun `fetch sends GET request to provided URL`() = runTest { + val mockEngine = MockEngine { request -> + assertEquals("https://kotlinlang.org/docs/_llms/coroutines-overview.txt", request.url.toString()) + assertEquals(HttpMethod.Get, request.method) + + respond( + content = "# Coroutines overview", + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "text/plain"), + ) + } + + val fetcher = createFetcher(mockEngine) + fetcher.fetch("https://kotlinlang.org/docs/_llms/coroutines-overview.txt") + } + + @Test + fun `fetch returns response body as text`() = runTest { + val pageContent = "# Coroutines\n\nKotlin provides coroutines support at the language level." + + val mockEngine = MockEngine { + respond( + content = pageContent, + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "text/plain"), + ) + } + + val fetcher = createFetcher(mockEngine) + val result = fetcher.fetch("https://kotlinlang.org/docs/_llms/coroutines-overview.txt") + + assertEquals(pageContent, result) + } + + @Test + fun `fetch throws ClientRequestException on 404`() = runTest { + val mockEngine = MockEngine { + respond( + content = "Not Found", + status = HttpStatusCode.NotFound, + headers = headersOf(HttpHeaders.ContentType, "text/plain"), + ) + } + + val fetcher = createFetcher(mockEngine) + assertFailsWith { + fetcher.fetch("https://kotlinlang.org/docs/_llms/nonexistent.txt") + } + } + + @Test + fun `fetch throws ServerResponseException on 5xx`() = runTest { + val mockEngine = MockEngine { + respond( + content = "Internal Server Error", + status = HttpStatusCode.InternalServerError, + headers = headersOf(HttpHeaders.ContentType, "text/plain"), + ) + } + + val fetcher = createFetcher(mockEngine) + assertFailsWith { + fetcher.fetch("https://kotlinlang.org/docs/_llms/coroutines-overview.txt") + } + } +} diff --git a/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/content/UrlMapperTest.kt b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/content/UrlMapperTest.kt new file mode 100644 index 0000000..c2076a3 --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/content/UrlMapperTest.kt @@ -0,0 +1,91 @@ +package org.kotlinlang.mcp.content + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class UrlMapperTest { + + @Test + fun `top-level path maps to _llms URL`() { + assertEquals( + "https://kotlinlang.org/docs/_llms/coroutines-overview.txt", + mapPathToUrl("coroutines-overview"), + ) + } + + @Test + fun `nested path maps to _llms URL`() { + assertEquals( + "https://kotlinlang.org/docs/multiplatform/_llms/compose.txt", + mapPathToUrl("multiplatform/compose"), + ) + } + + @Test + fun `deep nested path maps to _llms URL`() { + assertEquals( + "https://kotlinlang.org/docs/a/b/_llms/c.txt", + mapPathToUrl("a/b/c"), + ) + } + + @Test + fun `trims leading and trailing slashes`() { + assertEquals( + "https://kotlinlang.org/docs/_llms/coroutines-overview.txt", + mapPathToUrl("/coroutines-overview/"), + ) + } + + @Test + fun `strips html extension`() { + assertEquals( + "https://kotlinlang.org/docs/_llms/coroutines-overview.txt", + mapPathToUrl("coroutines-overview.html"), + ) + } + + @Test + fun `normalizes slashes and html extension together`() { + assertEquals( + "https://kotlinlang.org/docs/multiplatform/_llms/compose.txt", + mapPathToUrl("/multiplatform/compose.html"), + ) + } + + @Test + fun `empty string throws IllegalArgumentException`() { + assertFailsWith { + mapPathToUrl("") + } + } + + @Test + fun `only slashes throws IllegalArgumentException`() { + assertFailsWith { + mapPathToUrl("///") + } + } + + @Test + fun `blank path throws IllegalArgumentException`() { + assertFailsWith { + mapPathToUrl(" ") + } + } + + @Test + fun `path traversal throws IllegalArgumentException`() { + assertFailsWith { + mapPathToUrl("../../secret") + } + } + + @Test + fun `path traversal in the middle throws IllegalArgumentException`() { + assertFailsWith { + mapPathToUrl("multiplatform/../secret") + } + } +} diff --git a/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/tools/GetKotlinlangPageTest.kt b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/tools/GetKotlinlangPageTest.kt new file mode 100644 index 0000000..18f0cfe --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/tools/GetKotlinlangPageTest.kt @@ -0,0 +1,139 @@ +package org.kotlinlang.mcp.tools + +import io.ktor.client.* +import io.ktor.client.engine.mock.* +import io.ktor.http.* +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import kotlinx.coroutines.test.runTest +import org.kotlinlang.mcp.cache.TtlCache +import org.kotlinlang.mcp.content.PageFetcher +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.hours + +class GetKotlinlangPageTest { + + private fun createTool(mockEngine: MockEngine): GetKotlinlangPage { + val httpClient = HttpClient(mockEngine) { + expectSuccess = true + } + val pageFetcher = PageFetcher(httpClient) + val cache = TtlCache(ttl = 1.hours) + return GetKotlinlangPage(pageFetcher, cache) + } + + @Test + fun `handle returns page content for valid path`() = runTest { + val pageContent = "# Coroutines\n\nKotlin provides coroutines support." + val mockEngine = MockEngine { request -> + assertEquals( + "https://kotlinlang.org/docs/_llms/coroutines-overview.txt", + request.url.toString(), + ) + respond( + content = pageContent, + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "text/plain"), + ) + } + val tool = createTool(mockEngine) + + val result = tool.handle("coroutines-overview") + val text = (result.content.first() as TextContent).text + + assertNull(result.isError) + assertEquals(pageContent, text) + } + + @Test + fun `handle returns cached content on second call`() = runTest { + var callCount = 0 + val mockEngine = MockEngine { + callCount++ + respond( + content = "# Page content", + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "text/plain"), + ) + } + val tool = createTool(mockEngine) + + val result1 = tool.handle("coroutines-overview") + val result2 = tool.handle("coroutines-overview") + + assertEquals(1, callCount) + assertEquals( + (result1.content.first() as TextContent).text, + (result2.content.first() as TextContent).text, + ) + } + + @Test + fun `handle returns error with hint on 404`() = runTest { + val mockEngine = MockEngine { + respond( + content = "Not Found", + status = HttpStatusCode.NotFound, + headers = headersOf(HttpHeaders.ContentType, "text/plain"), + ) + } + val tool = createTool(mockEngine) + + val result = tool.handle("nonexistent-page") + val text = (result.content.first() as TextContent).text + + assertEquals(true, result.isError) + assertTrue(text.contains("Page not found: nonexistent-page")) + assertTrue(text.contains("search_kotlinlang")) + } + + @Test + fun `handle returns error on network failure`() = runTest { + val mockEngine = MockEngine { + respond( + content = "Internal Server Error", + status = HttpStatusCode.InternalServerError, + headers = headersOf(HttpHeaders.ContentType, "text/plain"), + ) + } + val tool = createTool(mockEngine) + + val result = tool.handle("coroutines-overview") + + assertEquals(true, result.isError) + val text = (result.content.first() as TextContent).text + assertTrue(text.startsWith("Failed to fetch page:")) + } + + @Test + fun `handle returns error on empty path`() = runTest { + val mockEngine = MockEngine { + respond( + content = "should not reach", + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "text/plain"), + ) + } + val tool = createTool(mockEngine) + + val result = tool.handle("") + + assertEquals(true, result.isError) + val text = (result.content.first() as TextContent).text + assertTrue(text.contains("Invalid path:")) + } + + @Test + fun `handle rethrows CancellationException`() = runTest { + val mockEngine = MockEngine { + throw kotlinx.coroutines.CancellationException("cancelled") + } + val tool = createTool(mockEngine) + + kotlin.test.assertFailsWith { + tool.handle("coroutines-overview") + } + } +} diff --git a/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/tools/SearchKotlinlangTest.kt b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/tools/SearchKotlinlangTest.kt new file mode 100644 index 0000000..7e10420 --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/tools/SearchKotlinlangTest.kt @@ -0,0 +1,231 @@ +package org.kotlinlang.mcp.tools + +import io.ktor.client.* +import io.ktor.client.engine.mock.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json +import org.kotlinlang.mcp.algolia.AlgoliaClient +import org.kotlinlang.mcp.cache.TtlCache +import org.kotlinlang.mcp.config.ServerConfig +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.minutes +import kotlin.time.TestTimeSource + +class SearchKotlinlangTest { + + private val testConfig = ServerConfig( + algoliaAppId = "test-app-id", + algoliaApiKey = "test-api-key", + algoliaIndexName = "test-index", + ) + + private val json = Json { ignoreUnknownKeys = true } + + private fun createSearchTool( + mockEngine: MockEngine, + timeSource: TestTimeSource = TestTimeSource(), + ): SearchKotlinlang { + val httpClient = HttpClient(mockEngine) { + install(ContentNegotiation) { json(json) } + } + val algoliaClient = AlgoliaClient(testConfig, httpClient) + val cache = TtlCache(ttl = 10.minutes, timeSource = timeSource) + return SearchKotlinlang(algoliaClient, cache) + } + + private fun mockEngineWithHits(hitsJson: String): MockEngine = MockEngine { + respond( + content = """{"hits": [$hitsJson]}""", + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json"), + ) + } + + private fun hit( + objectID: String = "id1", + mainTitle: String? = "Title", + url: String = "/docs/test.html", + snippet: String? = "some text", + matchLevel: String = "full", + ): String = buildString { + append("""{"objectID":"$objectID","url":"$url"""") + if (mainTitle != null) append(""","mainTitle":"$mainTitle"""") + if (snippet != null) { + append(""","_snippetResult":{"content":{"value":"$snippet","matchLevel":"$matchLevel"}}""") + } + append("}") + } + + @Test + fun `filters out non-docs hits`() = runTest { + val engine = mockEngineWithHits( + listOf( + hit(objectID = "1", url = "/docs/coroutines.html", mainTitle = "Coroutines"), + hit(objectID = "2", url = "https://kotlinlang.org/api/core/kotlin/", mainTitle = "API"), + hit(objectID = "3", url = "/docs/flow.html", mainTitle = "Flow"), + ).joinToString(",") + ) + val tool = createSearchTool(engine) + + val result = tool.handle("coroutines") + val text = result.content.first().let { (it as io.modelcontextprotocol.kotlin.sdk.types.TextContent).text } + + assertTrue(text.contains("Coroutines")) + assertTrue(text.contains("Flow")) + assertTrue(!text.contains("API")) + } + + @Test + fun `takes only top 5 after filtering`() = runTest { + val hits = (1..10).map { i -> + hit(objectID = "id$i", url = "/docs/page$i.html", mainTitle = "Page $i", snippet = "text $i") + } + val engine = mockEngineWithHits(hits.joinToString(",")) + val tool = createSearchTool(engine) + + val result = tool.handle("kotlin") + val text = result.content.first().let { (it as io.modelcontextprotocol.kotlin.sdk.types.TextContent).text } + + for (i in 1..5) assertTrue(text.contains("Page $i"), "Should contain Page $i") + for (i in 6..10) assertTrue(!text.contains("Page $i"), "Should not contain Page $i") + } + + @Test + fun `formats results with title path and snippet`() = runTest { + val engine = mockEngineWithHits( + hit( + objectID = "1", + url = "/docs/coroutines-overview.html", + mainTitle = "Coroutines overview", + snippet = "Kotlin provides coroutines support", + ) + ) + val tool = createSearchTool(engine) + + val result = tool.handle("coroutines") + val text = result.content.first().let { (it as io.modelcontextprotocol.kotlin.sdk.types.TextContent).text } + + assertEquals( + """ + 1. Coroutines overview [coroutines-overview] + Kotlin provides coroutines support + """.trimIndent(), + text, + ) + } + + @Test + fun `strips HTML tags from snippet`() = runTest { + val engine = mockEngineWithHits( + hit(snippet = "coroutines are great") + ) + val tool = createSearchTool(engine) + + val result = tool.handle("coroutines") + val text = result.content.first().let { (it as io.modelcontextprotocol.kotlin.sdk.types.TextContent).text } + + assertTrue(text.contains("coroutines are great")) + assertTrue(!text.contains("")) + assertTrue(!text.contains("")) + } + + @Test + fun `uses Untitled when mainTitle is null`() = runTest { + val engine = mockEngineWithHits( + hit(mainTitle = null, url = "/docs/basics.html", snippet = "some text") + ) + val tool = createSearchTool(engine) + + val result = tool.handle("basics") + val text = result.content.first().let { (it as io.modelcontextprotocol.kotlin.sdk.types.TextContent).text } + + assertTrue(text.contains("Untitled [basics]")) + } + + @Test + fun `omits snippet line when snippetResult is null`() = runTest { + val engine = mockEngineWithHits( + hit(url = "/docs/basics.html", mainTitle = "Basics", snippet = null) + ) + val tool = createSearchTool(engine) + + val result = tool.handle("basics") + val text = result.content.first().let { (it as io.modelcontextprotocol.kotlin.sdk.types.TextContent).text } + + assertEquals("1. Basics [basics]", text) + } + + @Test + fun `returns no results message when all hits are filtered out`() = runTest { + val engine = mockEngineWithHits( + hit(url = "https://kotlinlang.org/api/core/kotlin/", mainTitle = "API") + ) + val tool = createSearchTool(engine) + + val result = tool.handle("something") + val text = result.content.first().let { (it as io.modelcontextprotocol.kotlin.sdk.types.TextContent).text } + + assertNull(result.isError) + assertEquals("No results found for: something", text) + } + + @Test + fun `returns isError true when Algolia fails`() = runTest { + val engine = MockEngine { + respond( + content = """{"message": "Internal server error"}""", + status = HttpStatusCode.InternalServerError, + headers = headersOf(HttpHeaders.ContentType, "application/json"), + ) + } + val tool = createSearchTool(engine) + + val result = tool.handle("coroutines") + + assertEquals(true, result.isError) + val text = result.content.first().let { (it as io.modelcontextprotocol.kotlin.sdk.types.TextContent).text } + assertTrue(text.startsWith("Search failed:")) + } + + @Test + fun `caches result and does not call Algolia on second request`() = runTest { + var callCount = 0 + val engine = MockEngine { + callCount++ + respond( + content = """{"hits": [${hit(url = "/docs/test.html")}]}""", + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json"), + ) + } + val tool = createSearchTool(engine) + + tool.handle("coroutines") + tool.handle("coroutines") + + assertEquals(1, callCount) + } + + @Test + fun `extracts path from nested URL`() = runTest { + val engine = mockEngineWithHits( + hit( + url = "/docs/multiplatform/compose.html", + mainTitle = "Compose", + snippet = "text", + ) + ) + val tool = createSearchTool(engine) + + val result = tool.handle("compose") + val text = result.content.first().let { (it as io.modelcontextprotocol.kotlin.sdk.types.TextContent).text } + + assertTrue(text.contains("[multiplatform/compose]")) + } +} diff --git a/samples/notebooks/McpClient.ipynb b/samples/notebooks/McpClient.ipynb new file mode 100644 index 0000000..54a0953 --- /dev/null +++ b/samples/notebooks/McpClient.ipynb @@ -0,0 +1,733 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "# MCP Client Example\n", + "\n", + "Here is the example of how you can use Kotlin MCP client.\n", + "\n", + "Let's start with adding dependencies and imports.\n", + "\n", + "We'll need:\n", + "- Kotlin SDK Client\n", + "- Ktor client\n", + "- Kotlinx.coroutines" + ] + }, + { + "metadata": { + "executionRelatedData": { + "compiledClasses": [ + "Line_3_jupyter", + "Line_7_jupyter", + "Line_10_jupyter", + "Line_5_jupyter", + "Line_8_jupyter", + "Line_6_jupyter", + "Line_11_jupyter", + "Line_4_jupyter", + "Line_9_jupyter", + "Line_18_jupyter", + "Line_19_jupyter" + ] + }, + "ExecuteTime": { + "end_time": "2026-03-30T10:42:19.114283Z", + "start_time": "2026-03-30T10:42:19.068020Z" + } + }, + "cell_type": "code", + "source": [ + "%use coroutines (1.10.2)\n", + "%use ktor-client (0.3.0-98)\n", + "\n", + "USE {\n", + " dependencies(\n", + " \"io.modelcontextprotocol:kotlin-sdk-client-jvm:0.10.0\",\n", + " )\n", + "}" + ], + "outputs": [], + "execution_count": 8 + }, + { + "metadata": { + "executionRelatedData": { + "compiledClasses": [ + "Line_12_jupyter", + "Line_20_jupyter" + ] + }, + "ExecuteTime": { + "end_time": "2026-03-30T10:42:19.131015Z", + "start_time": "2026-03-30T10:42:19.115475Z" + } + }, + "cell_type": "code", + "source": [ + "import io.ktor.client.HttpClient\n", + "import io.ktor.client.plugins.logging.*\n", + "import io.ktor.client.plugins.sse.SSE\n", + "import io.modelcontextprotocol.kotlin.sdk.client.Client\n", + "import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport\n", + "import io.modelcontextprotocol.kotlin.sdk.types.Implementation\n", + "import io.modelcontextprotocol.kotlin.sdk.types.TextContent" + ], + "outputs": [], + "execution_count": 9 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "First, let's create the Ktor HTTP Client first.\n", + "We are installing Server-Sent Events (SSE) plugin, and, also, a Logging plugin for convenience." + ] + }, + { + "metadata": { + "executionRelatedData": { + "compiledClasses": [ + "Line_13_jupyter", + "Line_21_jupyter" + ] + }, + "ExecuteTime": { + "end_time": "2026-03-30T10:42:19.189250Z", + "start_time": "2026-03-30T10:42:19.131969Z" + } + }, + "cell_type": "code", + "source": [ + "val httpClient = HttpClient {\n", + " install(SSE)\n", + " install(Logging) {\n", + " level = LogLevel.ALL\n", + " logger = object : Logger {\n", + " override fun log(message: String) {\n", + " val messageType = if (message.contains(\"REQUEST\")) \"Request\" else \"Response\"\n", + " DISPLAY(HTML(\"
HTTP $messageType
$message
\"))\n", + " }\n", + " }\n", + " }\n", + "}\n" + ], + "outputs": [], + "execution_count": 10 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Now let's create an MCP client:" + }, + { + "metadata": { + "executionRelatedData": { + "compiledClasses": [ + "Line_14_jupyter", + "Line_22_jupyter" + ] + }, + "ExecuteTime": { + "end_time": "2026-03-30T10:42:19.216018Z", + "start_time": "2026-03-30T10:42:19.190636Z" + } + }, + "cell_type": "code", + "source": [ + "val mcpClient = Client(\n", + " clientInfo = Implementation(\n", + " name = \"my-client\",\n", + " version = \"1.0.0\"\n", + " )\n", + ")" + ], + "outputs": [], + "execution_count": 11 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Now we can connect an MCP client to remote MCP server via StreamableHttpClientTransport" + }, + { + "metadata": { + "executionRelatedData": { + "compiledClasses": [ + "Line_15_jupyter", + "Line_23_jupyter" + ] + }, + "ExecuteTime": { + "end_time": "2026-03-30T10:42:19.748081Z", + "start_time": "2026-03-30T10:42:19.216824Z" + } + }, + "cell_type": "code", + "source": [ + "runBlocking {\n", + " mcpClient.connect(\n", + " StreamableHttpClientTransport(\n", + " client = httpClient,\n", + " url = \"https://learn.microsoft.com/api/mcp\"\n", + " )\n", + " )\n", + "\n", + " println(\"✅ Server version: ${mcpClient.serverVersion}\")\n", + " println(\"✅ Server capabilities: ${mcpClient.serverCapabilities}\")\n", + "}" + ], + "outputs": [ + { + "data": { + "text/html": [ + "
HTTP Request
REQUEST: https://learn.microsoft.com/api/mcp\n",
+       "METHOD: POST\n",
+       "COMMON HEADERS\n",
+       "-> Accept: application/json, text/event-stream\n",
+       "-> Accept-Charset: UTF-8\n",
+       "CONTENT HEADERS\n",
+       "-> Content-Length: 191\n",
+       "-> Content-Type: application/json\n",
+       "BODY Content-Type: application/json\n",
+       "BODY START\n",
+       "{\"id\":\"8461d63b3d784df183832d109b688608\",\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2025-11-25\",\"capabilities\":{},\"clientInfo\":{\"name\":\"my-client\",\"version\":\"1.0.0\"}},\"jsonrpc\":\"2.0\"}\n",
+       "BODY END
" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/html": [ + "
HTTP Response
RESPONSE: 200 OK\n",
+       "METHOD: POST\n",
+       "FROM: https://learn.microsoft.com/api/mcp\n",
+       "COMMON HEADERS\n",
+       "-> Akamai-Cache-Status: Miss from child, NotCacheable from parent\n",
+       "-> Cache-Control: no-cache, no-store\n",
+       "-> Connection: keep-alive\n",
+       "-> Content-Encoding: identity\n",
+       "-> Content-Length: 1983\n",
+       "-> Content-Type: text/event-stream\n",
+       "-> Date: Mon, 30 Mar 2026 10:42:19 GMT\n",
+       "-> Expires: Mon, 30 Mar 2026 10:42:19 GMT\n",
+       "-> Request-Context: appId=cid-v1:ccd19013-8f20-4877-8d3f-84a0107e7119\n",
+       "-> Set-Cookie: ASLBSA=0003562b174eb06eed9ae0ec705e26d2c9aec6332388d120314730a5c2da183d42c8; Path=/; Secure; HttpOnly;; ASLBSACORS=0003562b174eb06eed9ae0ec705e26d2c9aec6332388d120314730a5c2da183d42c8; SameSite=none; Path=/; Secure; HttpOnly;\n",
+       "-> Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\n",
+       "-> X-Content-Type-Options: nosniff\n",
+       "-> X-Powered-By: ASP.NET\n",
+       "-> mcp-session-id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIiwiaWNvbnMiOm51bGwsIndlYnNpdGVVcmwiOm51bGx9LCJ1c2VySWRDbGFpbSI6IjQ4NjFlZWMxLWRlNGUtNGZhMi1hMjA4LWJkNzNhNDdkYmE3OCJ9\n",
+       "-> nel: {\"report_to\":\"network-errors\",\"max_age\":604800,\"success_fraction\":0.01,\"failure_fraction\":1.0}\n",
+       "-> report-to: {\"group\":\"network-errors\",\"max_age\":604800,\"endpoints\":[{\"url\":\"https://mdec.nelreports.net/api/report?cat=mdocs\"}]}\n",
+       "-> x-azure-ref: 20260330T104219Z-157fb874dd7qc6ndhC1SVG8pe00000000740000000000xwt\n",
+       "BODY Content-Type: text/event-stream\n",
+       "BODY START\n",
+       "event: message\n",
+       "data: {\"result\":{\"protocolVersion\":\"2025-06-18\",\"capabilities\":{\"logging\":{},\"prompts\":{\"listChanged\":true},\"resources\":{\"listChanged\":true},\"tools\":{\"listChanged\":true}},\"serverInfo\":{\"name\":\"Microsoft Learn MCP Server\",\"version\":\"1.0.0\"},\"instructions\":\"# Microsoft Learn MCP Server\\r\\n\\r\\nThis server gives structured access to official Microsoft and Azure documentation via three tools:\\r\\n\\r\\n## Tools\\r\\n\\r\\n### microsoft_docs_search\\r\\nSearch official documentation and return up to 10 concise, high-quality content chunks (max 500 tokens each), including title, URL, and excerpt.\\r\\n\\r\\n- Use first to get a quick, reliable overview\\r\\n- Ideal for grounding answers in Microsoft knowledge\\r\\n\\r\\n### microsoft_code_sample_search\\r\\nSearch for code snippets and examples in official Microsoft Learn documentation and return up to 20 relevant, high-quality code samples.\\r\\n\\r\\n- Use when you need to provide sample Microsoft/Azure related code in your answers.  \\r\\n- Ideal for generating code snippets or practical implementation examples.  \\r\\n- Optional parameter \\u0060language\\u0060 can filter results. \\r\\n\\r\\n### microsoft_docs_fetch\\r\\nFetch and convert full Microsoft documentation pages to markdown.\\r\\n\\r\\n- Use after search when you need full content from a specific URL\\r\\n- Required for detailed tutorials, troubleshooting, prerequisites, code samples, or when search results are incomplete or outdated\\r\\n\\r\\n## Workflow\\r\\n\\r\\n1. Use \\u0060microsoft_docs_search\\u0060 to find relevant documents.\\r\\n2. If you need code examples or practical snippets, use \\u0060microsoft_code_sample_search\\u0060.\\r\\n3. If deeper or complete information is needed, use \\u0060microsoft_docs_fetch\\u0060.\\r\\n\\r\\n**Search gives breadth. Code Sample Search gives practical examples. Fetch gives depth.**\\r\\n\\r\\nAll content comes from Microsoft Learn or official sources, returned in clean markdown format.\"},\"id\":\"8461d63b3d784df183832d109b688608\",\"jsonrpc\":\"2.0\"}\n",
+       "\n",
+       "\n",
+       "BODY END
" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/html": [ + "
HTTP Request
REQUEST: https://learn.microsoft.com/api/mcp\n",
+       "METHOD: POST\n",
+       "COMMON HEADERS\n",
+       "-> Accept: application/json, text/event-stream\n",
+       "-> Accept-Charset: UTF-8\n",
+       "-> mcp-session-id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIiwiaWNvbnMiOm51bGwsIndlYnNpdGVVcmwiOm51bGx9LCJ1c2VySWRDbGFpbSI6IjQ4NjFlZWMxLWRlNGUtNGZhMi1hMjA4LWJkNzNhNDdkYmE3OCJ9\n",
+       "CONTENT HEADERS\n",
+       "-> Content-Length: 54\n",
+       "-> Content-Type: application/json\n",
+       "BODY Content-Type: application/json\n",
+       "BODY START\n",
+       "{\"method\":\"notifications/initialized\",\"jsonrpc\":\"2.0\"}\n",
+       "BODY END
" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/html": [ + "
HTTP Response
RESPONSE: 202 Accepted\n",
+       "METHOD: POST\n",
+       "FROM: https://learn.microsoft.com/api/mcp\n",
+       "COMMON HEADERS\n",
+       "-> Akamai-Cache-Status: Miss from child, NotCacheable from parent\n",
+       "-> Cache-Control: no-cache, no-store\n",
+       "-> Connection: keep-alive\n",
+       "-> Content-Encoding: identity\n",
+       "-> Content-Length: 0\n",
+       "-> Date: Mon, 30 Mar 2026 10:42:19 GMT\n",
+       "-> Expires: Mon, 30 Mar 2026 10:42:19 GMT\n",
+       "-> Request-Context: appId=cid-v1:ccd19013-8f20-4877-8d3f-84a0107e7119\n",
+       "-> Set-Cookie: ASLBSA=0003562b174eb06eed9ae0ec705e26d2c9aec6332388d120314730a5c2da183d42c8; Path=/; Secure; HttpOnly;; ASLBSACORS=0003562b174eb06eed9ae0ec705e26d2c9aec6332388d120314730a5c2da183d42c8; SameSite=none; Path=/; Secure; HttpOnly;\n",
+       "-> Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\n",
+       "-> X-Content-Type-Options: nosniff\n",
+       "-> X-Powered-By: ASP.NET\n",
+       "-> mcp-session-id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIiwiaWNvbnMiOm51bGwsIndlYnNpdGVVcmwiOm51bGx9LCJ1c2VySWRDbGFpbSI6IjQ4NjFlZWMxLWRlNGUtNGZhMi1hMjA4LWJkNzNhNDdkYmE3OCJ9\n",
+       "-> nel: {\"report_to\":\"network-errors\",\"max_age\":604800,\"success_fraction\":0.01,\"failure_fraction\":1.0}\n",
+       "-> report-to: {\"group\":\"network-errors\",\"max_age\":604800,\"endpoints\":[{\"url\":\"https://mdec.nelreports.net/api/report?cat=mdocs\"}]}\n",
+       "-> x-azure-ref: 20260330T104219Z-157fb874dd7dr7drhC1SVGcxmc0000000pkg000000005v30\n",
+       "BODY Content-Type: null\n",
+       "BODY START\n",
+       "\n",
+       "BODY END
" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✅ Server version: Implementation(name=Microsoft Learn MCP Server, version=1.0.0, title=null, websiteUrl=null, icons=null)\n" + ] + }, + { + "data": { + "text/html": [ + "
HTTP Request
REQUEST: https://learn.microsoft.com/api/mcp\n",
+       "METHOD: GET\n",
+       "COMMON HEADERS\n",
+       "-> Accept: application/json\n",
+       "-> Accept-Charset: UTF-8\n",
+       "-> mcp-session-id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIiwiaWNvbnMiOm51bGwsIndlYnNpdGVVcmwiOm51bGx9LCJ1c2VySWRDbGFpbSI6IjQ4NjFlZWMxLWRlNGUtNGZhMi1hMjA4LWJkNzNhNDdkYmE3OCJ9\n",
+       "CONTENT HEADERS\n",
+       "-> Content-Length: 0\n",
+       "-> Accept: text/event-stream\n",
+       "-> Cache-Control: no-store\n",
+       "BODY Content-Type: null\n",
+       "BODY START\n",
+       "\n",
+       "BODY END
" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✅ Server capabilities: ServerCapabilities(tools=Tools(listChanged=true), resources=Resources(listChanged=true, subscribe=null), prompts=Prompts(listChanged=true), logging={}, completions=null, experimental=null)\n" + ] + } + ], + "execution_count": 12 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Let's get a list of available tools:" + }, + { + "metadata": { + "executionRelatedData": { + "compiledClasses": [ + "Line_16_jupyter", + "Line_24_jupyter" + ] + }, + "ExecuteTime": { + "end_time": "2026-03-30T10:42:20.410862Z", + "start_time": "2026-03-30T10:42:19.750112Z" + } + }, + "cell_type": "code", + "source": [ + "runBlocking {\n", + " val listToolsResult = mcpClient.listTools()\n", + "\n", + " for (tool in listToolsResult.tools) {\n", + " println(\n", + " \"\"\"\n", + " |🔨Title: ${tool.title} (${tool.name})\n", + " |\n", + " |Description: ${tool.description}\n", + " |\n", + " |Input schema: ${tool.inputSchema}\n", + " |Output schema: ${tool.outputSchema}\n", + " |Icons: ${tool.icons?.joinToString(\", \")}\n", + " |Annotations: ${tool.annotations}\n", + " |Execution: ${tool.execution}\n", + " |\n", + " \"\"\".trimMargin()\n", + " )\n", + " }\n", + "}\n" + ], + "outputs": [ + { + "data": { + "text/html": [ + "
HTTP Request
REQUEST: https://learn.microsoft.com/api/mcp\n",
+       "METHOD: POST\n",
+       "COMMON HEADERS\n",
+       "-> Accept: application/json, text/event-stream\n",
+       "-> Accept-Charset: UTF-8\n",
+       "-> mcp-session-id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIiwiaWNvbnMiOm51bGwsIndlYnNpdGVVcmwiOm51bGx9LCJ1c2VySWRDbGFpbSI6IjQ4NjFlZWMxLWRlNGUtNGZhMi1hMjA4LWJkNzNhNDdkYmE3OCJ9\n",
+       "CONTENT HEADERS\n",
+       "-> Content-Length: 79\n",
+       "-> Content-Type: application/json\n",
+       "BODY Content-Type: application/json\n",
+       "BODY START\n",
+       "{\"id\":\"b9c6596da2514e04b08cbaa1defb300b\",\"method\":\"tools/list\",\"jsonrpc\":\"2.0\"}\n",
+       "BODY END
" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/html": [ + "
HTTP Response
RESPONSE https://learn.microsoft.com/api/mcp failed with exception: io.ktor.client.plugins.sse.SSEClientException: Expected status code 200 but was 405
" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/html": [ + "
HTTP Response
RESPONSE: 405 Method Not Allowed\n",
+       "METHOD: GET\n",
+       "FROM: https://learn.microsoft.com/api/mcp\n",
+       "COMMON HEADERS\n",
+       "-> Akamai-Cache-Status: Miss from child, NotCacheable from parent\n",
+       "-> Cache-Control: max-age=0\n",
+       "-> Connection: keep-alive\n",
+       "-> Content-Length: 215\n",
+       "-> Date: Mon, 30 Mar 2026 10:42:19 GMT\n",
+       "-> Expires: Mon, 30 Mar 2026 10:42:19 GMT\n",
+       "-> Request-Context: appId=cid-v1:ccd19013-8f20-4877-8d3f-84a0107e7119\n",
+       "-> Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\n",
+       "-> X-Content-Type-Options: nosniff\n",
+       "-> X-Powered-By: ASP.NET\n",
+       "-> nel: {\"report_to\":\"network-errors\",\"max_age\":604800,\"success_fraction\":0.01,\"failure_fraction\":1.0}\n",
+       "-> report-to: {\"group\":\"network-errors\",\"max_age\":604800,\"endpoints\":[{\"url\":\"https://mdec.nelreports.net/api/report?cat=mdocs\"}]}\n",
+       "-> x-azure-ref: 20260330T104219Z-157fb874dd7qc6ndhC1SVG8pe00000000740000000000xxc
" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/html": [ + "
HTTP Response
RESPONSE: 200 OK\n",
+       "METHOD: POST\n",
+       "FROM: https://learn.microsoft.com/api/mcp\n",
+       "COMMON HEADERS\n",
+       "-> Akamai-Cache-Status: Miss from child, NotCacheable from parent\n",
+       "-> Cache-Control: no-cache, no-store\n",
+       "-> Connection: keep-alive\n",
+       "-> Content-Encoding: identity\n",
+       "-> Content-Length: 4983\n",
+       "-> Content-Type: text/event-stream\n",
+       "-> Date: Mon, 30 Mar 2026 10:42:20 GMT\n",
+       "-> Expires: Mon, 30 Mar 2026 10:42:20 GMT\n",
+       "-> Request-Context: appId=cid-v1:ccd19013-8f20-4877-8d3f-84a0107e7119\n",
+       "-> Set-Cookie: ASLBSA=0003562b174eb06eed9ae0ec705e26d2c9aec6332388d120314730a5c2da183d42c8; Path=/; Secure; HttpOnly;; ASLBSACORS=0003562b174eb06eed9ae0ec705e26d2c9aec6332388d120314730a5c2da183d42c8; SameSite=none; Path=/; Secure; HttpOnly;\n",
+       "-> Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\n",
+       "-> X-Content-Type-Options: nosniff\n",
+       "-> X-Powered-By: ASP.NET\n",
+       "-> mcp-session-id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIiwiaWNvbnMiOm51bGwsIndlYnNpdGVVcmwiOm51bGx9LCJ1c2VySWRDbGFpbSI6IjQ4NjFlZWMxLWRlNGUtNGZhMi1hMjA4LWJkNzNhNDdkYmE3OCJ9\n",
+       "-> nel: {\"report_to\":\"network-errors\",\"max_age\":604800,\"success_fraction\":0.01,\"failure_fraction\":1.0}\n",
+       "-> report-to: {\"group\":\"network-errors\",\"max_age\":604800,\"endpoints\":[{\"url\":\"https://mdec.nelreports.net/api/report?cat=mdocs\"}]}\n",
+       "-> x-azure-ref: 20260330T104219Z-157fb874dd7dr7drhC1SVGcxmc0000000pkg000000005v38\n",
+       "BODY Content-Type: text/event-stream\n",
+       "BODY START\n",
+       "event: message\n",
+       "data: {\"result\":{\"tools\":[{\"name\":\"microsoft_docs_search\",\"title\":\"Microsoft Docs Search\",\"description\":\"Search official Microsoft/Azure documentation to find the most relevant and trustworthy content for a user\\u0027s query. This tool returns up to 10 high-quality content chunks (each max 500 tokens), extracted from Microsoft Learn and other official sources. Each result includes the article title, URL, and a self-contained content excerpt optimized for fast retrieval and reasoning. Always use this tool to quickly ground your answers in accurate, first-party Microsoft/Azure knowledge.\\n\\n## Follow-up Pattern\\nTo ensure completeness, use microsoft_docs_fetch when high-value pages are identified by search. The fetch tool complements search by providing the full detail. This is a required step for comprehensive results.\",\"inputSchema\":{\"type\":\"object\",\"properties\":{\"query\":{\"description\":\"a query or topic about Microsoft/Azure products, services, platforms, developer tools, frameworks, or APIs\",\"type\":\"string\",\"default\":null}}},\"outputSchema\":{\"type\":\"object\",\"properties\":{\"results\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":[\"string\",\"null\"]},\"title\":{\"type\":\"string\"},\"content\":{\"type\":[\"string\",\"null\"]},\"contentUrl\":{\"type\":\"string\"},\"content_omitted\":{\"type\":\"boolean\"},\"extensionData\":{\"type\":[\"object\",\"null\"]}}}}}},\"annotations\":{\"title\":\"Microsoft Docs Search\",\"destructiveHint\":false,\"idempotentHint\":true,\"readOnlyHint\":true}},{\"name\":\"microsoft_code_sample_search\",\"title\":\"Microsoft Code Sample Search\",\"description\":\"Search for code snippets and examples in official Microsoft Learn documentation. This tool retrieves relevant code samples from Microsoft documentation pages providing developers with practical implementation examples and best practices for Microsoft/Azure products and services related coding tasks. This tool will help you use the **LATEST OFFICIAL** code snippets to empower coding capabilities.\\n\\n## When to Use This Tool\\n- When you are going to provide sample Microsoft/Azure related code snippets in your answers.\\n- When you are **generating any Microsoft/Azure related code**.\\n\\n## Usage Pattern\\nInput a descriptive query, or SDK/class/method name to retrieve related code samples. The optional parameter \\u0060language\\u0060 can help to filter results.\\n\\nEligible values for \\u0060language\\u0060 parameter include: csharp javascript typescript python powershell azurecli al sql java kusto cpp go rust ruby php\",\"inputSchema\":{\"type\":\"object\",\"properties\":{\"query\":{\"description\":\"a descriptive query, SDK name, method name or code snippet related to Microsoft/Azure products, services, platforms, developer tools, frameworks, APIs or SDKs\",\"type\":\"string\"},\"language\":{\"description\":\"Optional parameter specifying the programming language of code snippets to retrieve. Can significantly improve search quality if provided. Eligible values: csharp javascript typescript python powershell azurecli al sql java kusto cpp go rust ruby php\",\"type\":\"string\",\"default\":null}},\"required\":[\"query\"]},\"outputSchema\":{\"type\":\"object\",\"properties\":{\"results\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"description\":{\"type\":\"string\"},\"codeSnippet\":{\"type\":[\"string\",\"null\"]},\"link\":{\"type\":\"string\"},\"language\":{\"type\":\"string\"},\"content_omitted\":{\"type\":\"boolean\"}}}}}},\"annotations\":{\"title\":\"Microsoft Code Sample Search\",\"destructiveHint\":false,\"idempotentHint\":true,\"readOnlyHint\":true}},{\"name\":\"microsoft_docs_fetch\",\"title\":\"Microsoft Docs Fetch\",\"description\":\"Fetch and convert a Microsoft Learn documentation webpage to markdown format. This tool retrieves the latest complete content of Microsoft documentation webpages including Azure, .NET, Microsoft 365, and other Microsoft technologies.\\n\\n## When to Use This Tool\\n- When search results provide incomplete information or truncated content\\n- When you need complete step-by-step procedures or tutorials\\n- When you need troubleshooting sections, prerequisites, or detailed explanations\\n- When search results reference a specific page that seems highly relevant\\n- For comprehensive guides that require full context\\n\\n## Usage Pattern\\nUse this tool AFTER microsoft_docs_search when you identify specific high-value pages that need complete content. The search tool gives you an overview; this tool gives you the complete picture.\\n\\n## URL Requirements\\n- The URL must be a valid HTML documentation webpage from the microsoft.com domain\\n- Binary files (PDF, DOCX, images, etc.) are not supported\\n\\n## Output Format\\nmarkdown with headings, code blocks, tables, and links preserved.\",\"inputSchema\":{\"type\":\"object\",\"properties\":{\"url\":{\"description\":\"URL of the Microsoft documentation page to read\",\"type\":\"string\"}},\"required\":[\"url\"]},\"annotations\":{\"title\":\"Microsoft Docs Fetch\",\"destructiveHint\":false,\"idempotentHint\":true,\"readOnlyHint\":true}}]},\"id\":\"b9c6596da2514e04b08cbaa1defb300b\",\"jsonrpc\":\"2.0\"}\n",
+       "\n",
+       "\n",
+       "BODY END
" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "🔨Title: Microsoft Docs Search (microsoft_docs_search)\n", + "\n", + "Description: Search official Microsoft/Azure documentation to find the most relevant and trustworthy content for a user's query. This tool returns up to 10 high-quality content chunks (each max 500 tokens), extracted from Microsoft Learn and other official sources. Each result includes the article title, URL, and a self-contained content excerpt optimized for fast retrieval and reasoning. Always use this tool to quickly ground your answers in accurate, first-party Microsoft/Azure knowledge.\n", + "\n", + "## Follow-up Pattern\n", + "To ensure completeness, use microsoft_docs_fetch when high-value pages are identified by search. The fetch tool complements search by providing the full detail. This is a required step for comprehensive results.\n", + "\n", + "Input schema: ToolSchema(properties={\"query\":{\"description\":\"a query or topic about Microsoft/Azure products, services, platforms, developer tools, frameworks, or APIs\",\"type\":\"string\",\"default\":null}}, required=null, defs=null)\n", + "Output schema: ToolSchema(properties={\"results\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":[\"string\",\"null\"]},\"title\":{\"type\":\"string\"},\"content\":{\"type\":[\"string\",\"null\"]},\"contentUrl\":{\"type\":\"string\"},\"content_omitted\":{\"type\":\"boolean\"},\"extensionData\":{\"type\":[\"object\",\"null\"]}}}}}, required=null, defs=null)\n", + "Icons: null\n", + "Annotations: ToolAnnotations(title=Microsoft Docs Search, readOnlyHint=true, destructiveHint=false, idempotentHint=true, openWorldHint=null)\n", + "Execution: null\n", + "\n", + "🔨Title: Microsoft Code Sample Search (microsoft_code_sample_search)\n", + "\n", + "Description: Search for code snippets and examples in official Microsoft Learn documentation. This tool retrieves relevant code samples from Microsoft documentation pages providing developers with practical implementation examples and best practices for Microsoft/Azure products and services related coding tasks. This tool will help you use the **LATEST OFFICIAL** code snippets to empower coding capabilities.\n", + "\n", + "## When to Use This Tool\n", + "- When you are going to provide sample Microsoft/Azure related code snippets in your answers.\n", + "- When you are **generating any Microsoft/Azure related code**.\n", + "\n", + "## Usage Pattern\n", + "Input a descriptive query, or SDK/class/method name to retrieve related code samples. The optional parameter `language` can help to filter results.\n", + "\n", + "Eligible values for `language` parameter include: csharp javascript typescript python powershell azurecli al sql java kusto cpp go rust ruby php\n", + "\n", + "Input schema: ToolSchema(properties={\"query\":{\"description\":\"a descriptive query, SDK name, method name or code snippet related to Microsoft/Azure products, services, platforms, developer tools, frameworks, APIs or SDKs\",\"type\":\"string\"},\"language\":{\"description\":\"Optional parameter specifying the programming language of code snippets to retrieve. Can significantly improve search quality if provided. Eligible values: csharp javascript typescript python powershell azurecli al sql java kusto cpp go rust ruby php\",\"type\":\"string\",\"default\":null}}, required=[query], defs=null)\n", + "Output schema: ToolSchema(properties={\"results\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"description\":{\"type\":\"string\"},\"codeSnippet\":{\"type\":[\"string\",\"null\"]},\"link\":{\"type\":\"string\"},\"language\":{\"type\":\"string\"},\"content_omitted\":{\"type\":\"boolean\"}}}}}, required=null, defs=null)\n", + "Icons: null\n", + "Annotations: ToolAnnotations(title=Microsoft Code Sample Search, readOnlyHint=true, destructiveHint=false, idempotentHint=true, openWorldHint=null)\n", + "Execution: null\n", + "\n", + "🔨Title: Microsoft Docs Fetch (microsoft_docs_fetch)\n", + "\n", + "Description: Fetch and convert a Microsoft Learn documentation webpage to markdown format. This tool retrieves the latest complete content of Microsoft documentation webpages including Azure, .NET, Microsoft 365, and other Microsoft technologies.\n", + "\n", + "## When to Use This Tool\n", + "- When search results provide incomplete information or truncated content\n", + "- When you need complete step-by-step procedures or tutorials\n", + "- When you need troubleshooting sections, prerequisites, or detailed explanations\n", + "- When search results reference a specific page that seems highly relevant\n", + "- For comprehensive guides that require full context\n", + "\n", + "## Usage Pattern\n", + "Use this tool AFTER microsoft_docs_search when you identify specific high-value pages that need complete content. The search tool gives you an overview; this tool gives you the complete picture.\n", + "\n", + "## URL Requirements\n", + "- The URL must be a valid HTML documentation webpage from the microsoft.com domain\n", + "- Binary files (PDF, DOCX, images, etc.) are not supported\n", + "\n", + "## Output Format\n", + "markdown with headings, code blocks, tables, and links preserved.\n", + "\n", + "Input schema: ToolSchema(properties={\"url\":{\"description\":\"URL of the Microsoft documentation page to read\",\"type\":\"string\"}}, required=[url], defs=null)\n", + "Output schema: null\n", + "Icons: null\n", + "Annotations: ToolAnnotations(title=Microsoft Docs Fetch, readOnlyHint=true, destructiveHint=false, idempotentHint=true, openWorldHint=null)\n", + "Execution: null\n", + "\n" + ] + } + ], + "execution_count": 13 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Let's call a tool:" + }, + { + "metadata": { + "executionRelatedData": { + "compiledClasses": [ + "Line_17_jupyter", + "Line_25_jupyter" + ] + }, + "ExecuteTime": { + "end_time": "2026-03-30T10:42:21.227094Z", + "start_time": "2026-03-30T10:42:20.419561Z" + } + }, + "cell_type": "code", + "source": [ + "runBlocking {\n", + " val toolCallResult = mcpClient.callTool(\n", + " name = \"microsoft_docs_search\",\n", + " arguments = mapOf(\"query\" to \"Linux\")\n", + " )\n", + " for (content in toolCallResult.content) {\n", + " (content as? TextContent).let {\n", + " Json.parseToJsonElement(it?.text.orEmpty()).jsonObject.get(\"results\")?.jsonArray?.forEach { result ->\n", + " val item = result as JsonObject\n", + " println(\n", + " \"\"\"\n", + " |Title: ${item[\"title\"]}\n", + " |\n", + " |Content: ${item[\"content\"]}\n", + " |Content URL: ${item[\"contentUrl\"]}\n", + " \"\"\".trimMargin()\n", + " )\n", + " }\n", + " }\n", + " }\n", + "}\n" + ], + "outputs": [ + { + "data": { + "text/html": [ + "
HTTP Request
REQUEST: https://learn.microsoft.com/api/mcp\n",
+       "METHOD: POST\n",
+       "COMMON HEADERS\n",
+       "-> Accept: application/json, text/event-stream\n",
+       "-> Accept-Charset: UTF-8\n",
+       "-> mcp-session-id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIiwiaWNvbnMiOm51bGwsIndlYnNpdGVVcmwiOm51bGx9LCJ1c2VySWRDbGFpbSI6IjQ4NjFlZWMxLWRlNGUtNGZhMi1hMjA4LWJkNzNhNDdkYmE3OCJ9\n",
+       "CONTENT HEADERS\n",
+       "-> Content-Length: 162\n",
+       "-> Content-Type: application/json\n",
+       "BODY Content-Type: application/json\n",
+       "BODY START\n",
+       "{\"id\":\"07e26d19ee6248d98f6dc039bf4fc589\",\"method\":\"tools/call\",\"params\":{\"name\":\"microsoft_docs_search\",\"arguments\":{\"query\":\"Linux\"},\"_meta\":{}},\"jsonrpc\":\"2.0\"}\n",
+       "BODY END
" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/html": [ + "
HTTP Response
RESPONSE: 200 OK\n",
+       "METHOD: POST\n",
+       "FROM: https://learn.microsoft.com/api/mcp\n",
+       "COMMON HEADERS\n",
+       "-> Akamai-Cache-Status: Miss from child, NotCacheable from parent\n",
+       "-> Cache-Control: no-cache, no-store\n",
+       "-> Connection: keep-alive\n",
+       "-> Content-Encoding: identity\n",
+       "-> Content-Length: 49801\n",
+       "-> Content-Type: text/event-stream\n",
+       "-> Date: Mon, 30 Mar 2026 10:42:21 GMT\n",
+       "-> Expires: Mon, 30 Mar 2026 10:42:21 GMT\n",
+       "-> Request-Context: appId=cid-v1:ccd19013-8f20-4877-8d3f-84a0107e7119\n",
+       "-> Set-Cookie: ASLBSA=0003562b174eb06eed9ae0ec705e26d2c9aec6332388d120314730a5c2da183d42c8; Path=/; Secure; HttpOnly;; ASLBSACORS=0003562b174eb06eed9ae0ec705e26d2c9aec6332388d120314730a5c2da183d42c8; SameSite=none; Path=/; Secure; HttpOnly;\n",
+       "-> Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\n",
+       "-> X-Content-Type-Options: nosniff\n",
+       "-> X-Powered-By: ASP.NET\n",
+       "-> mcp-session-id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIiwiaWNvbnMiOm51bGwsIndlYnNpdGVVcmwiOm51bGx9LCJ1c2VySWRDbGFpbSI6IjQ4NjFlZWMxLWRlNGUtNGZhMi1hMjA4LWJkNzNhNDdkYmE3OCJ9\n",
+       "-> nel: {\"report_to\":\"network-errors\",\"max_age\":604800,\"success_fraction\":0.01,\"failure_fraction\":1.0}\n",
+       "-> report-to: {\"group\":\"network-errors\",\"max_age\":604800,\"endpoints\":[{\"url\":\"https://mdec.nelreports.net/api/report?cat=mdocs\"}]}\n",
+       "-> x-azure-ref: 20260330T104220Z-157fb874dd7dr7drhC1SVGcxmc0000000pkg000000005v3x\n",
+       "BODY Content-Type: text/event-stream\n",
+       "BODY START\n",
+       "event: message\n",
+       "data: {\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"{\\u0022results\\u0022:[{\\u0022title\\u0022:\\u0022How to download and install Linux\\u0022,\\u0022content\\u0022:\\u0022# How to download and install Linux\\\\n## Step 2 - Choose a Linux distribution\\\\nThe different versions of Linux are called \\\\u0022distributions\\\\u0022 (sometimes shortened to \\\\u201Cdistros\\\\u201D). So there isn\\\\u2019t actually a way to just \\\\u201Cinstall Linux\\\\u201D, you first need to choose which distribution that uses the Linux kernel you want to download and install. There are over 600 active Linux distributions, such as Ubuntu, Debian, Kali Linux, openSUSE, and more. There are a number of factors to consider in choosing a distribution, such as:\\\\n### Required Linux experience\\\\nIf you\\\\u2019re new to Linux, you may want to start with Ubuntu. This is one of the most common starting places for Linux users due to the level of support and ease of use. Ubuntu offers a desktop-version and a server-version depending on how you plan to use it. The specific tasks that the operating system will be responsible for may help you determine which distribution to use. Arch Linux is a popular choice for those who want a highly customizable, do-it-yourself approach that is still stable and well-supported by a large user base. It is a much more complicated place to start, but can help you to get a better understanding of how Linux works due to the amount of custom configuration.\\\\n### System requirements\\\\nA minimal, or \\\\u0022lightweight\\\\u0022, distribution means a small footprint, with lower memory and processing speed requirements. Alpine Linux is a popular option where users will find most features disabled or not installed by default, but still has everything you need for a containerized app. Security needs: Some Linux users may have specific security concerns to consider. Kali Linux is an example of a distro designed for security tasks, such as Penetration Testing, Security Research, Computer Forensics and Reverse Engineering.\\\\n### Business and Enterprise requirements\\\\nBusiness, corporate, or academic environments may have unique concerns from consumer environments. For example Red Hat Enterprise Linux (RHEL), SUSE Linux Enterprise Server (SLES) and Oracle Linux offer 24/7 global support and subscription-based service. CentOS is another popular enterprise distro that is community-supported but still compatible with RHEL.\\\\n### Community support\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/linux/install#step-2---choose-a-linux-distribution\\u0022},{\\u0022title\\u0022:\\u0022Part 1.2 - Basic operations on Linux\\u0022,\\u0022content\\u0022:\\u0022# Part 1.2 - Basic operations on Linux\\\\n## Run commands by using the \\\\u0022sudo\\\\u0022 prefix\\\\nInstead of becoming the root account user in your session, you can run commands as root by using the \\\\u0060sudo \\\\u003Ccommand\\\\u003E\\\\u0060 format. This approach is safer because it will run the given command only as a superuser.\\\\nFor example, if you run \\\\u0060restart\\\\u0060 while you\\\\u0027re not elevated to superuser status, the result is as follows.\\\\n![Screenshot of restart command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/restart-command.png)\\\\nTo run this command as a superuser instead, you can either change the session context to root (by running \\\\u0060sudo su\\\\u0060) or add the \\\\u0060sudo\\\\u0060 prefix, as follows:\\\\n\\\\u0060\\\\u0060\\\\u0060bash\\\\nsudo reboot\\\\n\\\\u0060\\\\u0060\\\\u0060\\\\nNow, the restart operation runs as expected by the virtual machine.\\\\n![Screenshot of reboot command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/reboot-command.png)\\\\n## Package managers\\\\nPackage managers are used to install, upgrade, and remove applications in Linux. There are several different package managers, such as DPKG (Debian Package Management System), RPM (Red Hat Package Manager), and Zypper for SUSE Package managers themselves are low-level. Therefore, it might be difficult to manage software directly by using the package manager. Fortunately, there are wrappers for these tools that make it easier to install and uninstall software. For example, [yum](https://wikipedia.org/wiki/Yum_%28software%29) is a wrapper around RPM (Red Hat Package Manager) and [APT](https://wikipedia.org/wiki/APT_%28software%29) is a wrapper around DPKG (Debian Package Management System).\\\\nThis tutorial assumes that you\\\\u0027re running the [Ubuntu package management system](https://ubuntu.com/server/docs/package-management). This system is derived from the same system that\\\\u0027s used by the Debian GNU/Linux distribution. Therefore, we can use APT to install the software.\\\\nDepending on your Linux choice, you might have to use other tools. For example, you can use yum to install or uninstall software if you\\\\u0027re running Red Hat.\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#run-commands-by-using-the-sudo-prefix\\u0022},{\\u0022title\\u0022:\\u0022Part 1.2 - Basic operations on Linux\\u0022,\\u0022content\\u0022:\\u0022# Part 1.2 - Basic operations on Linux\\\\n## Run commands as a superuser\\\\nThe superuser is the most privileged user account. It has root (unrestricted) access to all files and folders, and complete control over the operation of the computer. The system administrator uses this account for system maintenance.\\\\nOn UNIX-like systems, the conventional name of the superuser is \\\\u0022root\\\\u0022. The root user can do tasks that are otherwise restricted for standard users, including the following ones:\\\\n1. Changing system directories\\\\n2. Changing user privileges\\\\n3. Changing file ownership\\\\n4. System-related tasks such as restarts\\\\n5. Installation of certain applications\\\\nThere are several ways to run commands as the root account user. One is to use the \\\\u0060sudo su\\\\u0060 command. In this command, \\\\u0060sudo\\\\u0060 is short for \\\\u0060superuser do\\\\u0060, and \\\\u0060su\\\\u0060 is an abbreviation for \\\\u0022switch user.\\\\u0022 You can also use the \\\\u0060su\\\\u0060 command to switch between standard users.\\\\nImportant\\\\nAfter you become the root account user, everything you do runs in root context. Therefore, you should act carefully because the root account has access to the entire system.\\\\nTo demonstrate a permissions issue, create a text file in a special folder that\\\\u0027s named */etc*.\\\\nThe */etc/* folder is where system configuration files are located. This folder protects its contents from being changed by a standard user.\\\\nRun the following command:\\\\n\\\\u0060\\\\u0060\\\\u0060bash\\\\necho hello world \\\\u003E /etc/helloworld.txt\\\\n\\\\u0060\\\\u0060\\\\u0060\\\\nThe \\\\u0060echo\\\\u0060 command writes all the text that follows it to the output. The angle bracket (\\\\u0060\\\\u003E\\\\u0060) tells the system to send the output to the */etc/helloworld.txt* file instead of the console. This behavior is similar to how Windows works.\\\\nNote\\\\nLinux is case-sensitive: *Helloworld.txt*, *helloworld.txt* and *helloworld.Txt* are different files.\\\\nYou don\\\\u0027t have to provide a file name extension. The name \\\\u0022helloworld\\\\u0022 by itself is perfectly valid. (The \\\\u0022txt\\\\u0022 extension is used here only as an example.)\\\\nWhen you run this command, you receive a **permission denied** error message.\\\\n![Screenshot of permission denied message.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/permission-denied-message.png)\\\\nThe operation fails because the */etc* folder is special folder that a standard user can\\\\u0027t change. Verify the user by running the \\\\u0060id\\\\u0060 command.\\\\n![Screenshot of I D command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/id-command.png)\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#run-commands-as-a-superuser\\u0022},{\\u0022title\\u0022:\\u0022How to download and install Linux\\u0022,\\u0022content\\u0022:\\u0022# How to download and install Linux\\\\n## Step 1 - Choose a method to install Linux\\\\nThe choice of which method to use for installing Linux depends on your needs and preferences.\\\\n1. **New to using Linux?** We recommend starting with Windows Subsystem for Linux (WSL) as it\\\\u0027s the easiest way to get up and running.\\\\n2. **Work in a business environment with more complex needs related to scale or security?** We recommend running Linux as a Virtual Machine (VM) in the cloud and checking out the support that Azure has to offer. This also applies if you want to run Linux as a server.\\\\n3. **Only want to run Linux as your primary operating system?** If you are good with a slightly more complicated install process and don\\\\u0027t need access to Windows tools (like Outlook, Teams, Word, PowerPoint, etc), you can run Linux on bare metal to access the full potential of your hardware without any overhead from virtualization or emulation.\\\\nLearn more about these options below.\\\\n### Windows Subsystem for Linux (WSL)\\\\nAvailable to those with a Windows machine, this is the most simple way to install Linux. Just run the Linux install command: \\\\u0060wsl --install\\\\u0060 to install the Ubuntu distribution.\\\\nThis method uses virtualization to integrate the Windows operating system (OS) with the Linux OS (running on an actual Linux kernel). You can add as many different Linux distributions as you please and run your favorite Linux tools, including GUI apps, alongside your favorite Windows tools. You can also mix and match Bash and PowerShell commands in the same command line. A Linux file system hosts your project files, while a separate Windows file systems hosts any developer tools, such as VS Code, that you\\\\u0027d like to work on the files with. This method of running Linux is highly efficient and performant. Once you choose a Linux distribution or opt to use the default Ubuntu distro, follow the instructions to Use the install Linux command with Windows Subsystem for Linux or learn more in the [WSL documentation](https://learn.microsoft.com/en-us/windows/wsl).\\\\n![WSL install command screenshot](https://learn.microsoft.com/linux/images/wsl-install-command-screenshot.png)\\\\n### Create a Virtual Machine (VM)\\\\nAvailable to everyone regardless if you\\\\u0027re using a Windows or MacOS-based machine, but will require either an Azure account, if creating a VM in the cloud, or virtualization software, if creating a VM directly on your machine (locally).\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/linux/install#step-1---choose-a-method-to-install-linux\\u0022},{\\u0022title\\u0022:\\u0022How to download and install Linux\\u0022,\\u0022content\\u0022:\\u0022# How to download and install Linux\\\\nLinux is an operating system, similar to Windows, but with many different versions due to the nature of being open source and fully customizable. To install Linux, you must choose an install method and choose a Linux distribution.\\\\nTo install Linux:\\\\n1. Choose an install method: Windows Subsystem for Linux (WSL), Bare metal Linux; or create a Virtual Machine (VM) to run Linux locally or in the cloud.\\\\n2. Choose a Linux distribution: Ubuntu, Debian, Kali Linux, openSUSE, etc.\\\\n3. Follow the steps for your preferred install method:\\\\n3.1. Use the install Linux command with Windows Subsystem for Linux (WSL)\\\\n3.2. Create a Linux Virtual Machine (VM) in the cloud\\\\n3.3. Create a Linux Virtual Machine (VM) on your local machine\\\\n3.4. Create a bootable USB to install bare-metal Linux\\\\n4. After installing Linux: Get familiar with your distribution\\\\u0027s package manager, update and upgrade the packages available, and get familiar with the other [Linux resources at Microsoft](https://learn.microsoft.com/linux/), such as training courses, Linux-versions of popular tools, news, and Open Source events.\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/linux/install\\u0022},{\\u0022title\\u0022:\\u0022New to Linux resources for SQL users\\u0022,\\u0022content\\u0022:\\u0022# New to Linux resources for SQL users\\\\n**Applies to:**![](https://learn.microsoft.com/sql/includes/media/yes-icon.svg?view=sql-server-ver17)[SQL Server](https://learn.microsoft.com/sql/sql-server/sql-docs-navigation-guide?view=sql-server-ver17#applies-to) on Linux\\\\nThis article provides a learning path through an introduction to Linux concepts. Use the sections in the article as a guided learning path to familiarize yourself with the Linux environment.\\\\nThis list isn\\\\u0027t comprehensive but instead provides the bare minimum you need to help you administer and navigate your SQL Server on Linux environment. See the [full list of tutorials](https://www.linux.org/forums/linux-beginner-tutorials.123/) to learn more.\\\\n## What is Linux?\\\\nThe [What is Linux](https://www.linux.org/threads/what-is-linux.4106/) module introduces the history of Linux as an operating system. The module explains *the Kernel* and where Linux is today. This tutorial serves as an introduction to Linux, helping you get started.\\\\n## Select a distribution\\\\nAfter you know the history of Linux, decide which [distribution of Linux](https://www.linux.org/threads/selecting-a-linux-distribution.4117/) best suits your business needs.\\\\nSQL Server 2025 (17.x) is [supported](https://learn.microsoft.com/sql/linux/sql-server-linux-release-notes-2025?view=sql-server-ver17#supported-platforms) on Red Hat Enterprise Linux (RHEL) and Ubuntu.\\\\nSQL Server 2022 (16.x) and earlier versions are [also supported](https://learn.microsoft.com/sql/linux/sql-server-linux-release-notes-2022?view=sql-server-ver17#supported-platforms) on SUSE Linux Enterprise Server (SLES).\\\\n## Get around directories\\\\nAfter you choose the appropriate distribution of Linux, familiarize yourself with [getting around Linux directories](https://www.linux.org/threads/getting-around-in-linux-directories.4120/).\\\\nThis module helps you answer the following questions:\\\\n1. How to navigate between different files\\\\n2. How to identify a file from a directory\\\\n3. Identify different directories\\\\n## Install new software\\\\nNext, learn how to [install new software](https://www.linux.org/threads/installing-new-software-debian-red-hat-slackware.4119/) on your new Linux operating system. This module covers installing new software on the Debian and Red Hat Linux operating systems.\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/sql/linux/new-to-linux-resources?view=sql-server-ver17\\u0022},{\\u0022title\\u0022:\\u0022How to download and install Linux\\u0022,\\u0022content\\u0022:\\u0022# How to download and install Linux\\\\n## Step 3 - Follow install method instructions\\\\n### Install Linux with Windows Subsystem for Linux\\\\nTo install Linux on a Windows PC, use the install Linux command.\\\\n1. Open PowerShell or Windows Command Prompt in *administrator mode* by right-clicking and selecting \\\\u0022Run as administrator\\\\u0022.\\\\n2. Enter the Linux install command: \\\\u0060wsl --install\\\\u0060.\\\\n3. Restart your machine.\\\\n![WSL Linux Install List Command screenshot](https://learn.microsoft.com/linux/images/wsl-install-list-screenshot.png)\\\\n#### Install Linux command options\\\\nBy default, the install command will use the Ubuntu distribution of Linux. You can see the other distributions that are available in the Microsoft Store by entering the command: \\\\u0060wsl -l -o\\\\u0060. If you\\\\u0027d like to install a distribution other than Ubuntu, just add \\\\u0060-d\\\\u0060 to the command with the name of the distribution you\\\\u0027d like to install:\\\\n\\\\u0060\\\\u0060\\\\u0060powershell\\\\nwsl --install -d \\\\u003CDistro Name\\\\u003E\\\\n\\\\u0060\\\\u0060\\\\u0060\\\\nIf the distribution you want to install isn\\\\u0027t available in the Microsoft Store, see the Windows Subsystem for Linux (WSL) documentation on how to [Import any Linux distribution to use with WSL](https://learn.microsoft.com/en-us/windows/wsl/use-custom-distro).\\\\nFor more information on how to use VS Code, Git, databases, Docker, GUI apps, GPU acceleration, NodesJS, USB devices, or mount a disk, build a custom distribution, manage disk space, or set up WSL for your enterprise business, see the [WSL documentation](https://learn.microsoft.com/en-us/windows/wsl/).\\\\n### Create a Linux Virtual Machine in the cloud\\\\nTo create a Linux Virtual Machine (VM) that is hosted in the cloud on Azure, log-in to the Azure Portal and select the \\\\u201CCreate a Virtual Machine\\\\u201D service. Then follow the instructions for creating a user name, project, instance, SSH key, ports, and copying the IP address.\\\\nAzure supports a variety of options for setting up Linux VMs in the cloud. For a quickstart guide on how to set up a Linux VM, choose your preferred method:\\\\n1. [Azure Portal](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-portal?tabs=ubuntu\\\\u0026source=docs)\\\\n2. [Azure CLI](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-cli)\\\\n3. [Azure PowerShell](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-powershell)\\\\n4. [Terraform](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-terraform)\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/linux/install#step-3---follow-install-method-instructions\\u0022},{\\u0022title\\u0022:\\u0022How to download and install Linux\\u0022,\\u0022content\\u0022:\\u0022# How to download and install Linux\\\\n## Step 2 - Choose a Linux distribution\\\\nA large user community that consistently contributes to the distribution, fixing bugs, adding features, helping with questions, etc. is another good consideration. Ubuntu is one of the most popular distos. openSUSE has been around longer than most other distributions and offers in addition to the stable version (Leap) a rolling version (Tumbleweed). Many other distros rapidly grow popular as the needs and preferences of the user community change. The Linux.org site has resources to help you assess what\\\\u0027s available, well-supported, or growing in popularity.\\\\n![Screenshot of the list of distributions on Linux.org](https://learn.microsoft.com/linux/images/linux-distributions-list-screenshot.png)\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/linux/install#step-2---choose-a-linux-distribution\\u0022},{\\u0022title\\u0022:\\u0022Part 1.2 - Basic operations on Linux\\u0022,\\u0022content\\u0022:\\u0022# Part 1.2 - Basic operations on Linux\\\\n## Run commands as a superuser\\\\nTo become the root account user, run the \\\\u0060sudo su\\\\u0060 command.\\\\n![Screenshot of sudo su command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/sudo-su-command.png)\\\\nYou should notice two things: When you become root, the dollar sign character (\\\\u0060$\\\\u0060) becomes a pound sign character (\\\\u0060#\\\\u0060). Also, the username is changed to **root**. If you run the same \\\\u0060id\\\\u0060 command again, you see that the user and group ID of root are **0**.\\\\n![Screenshot of root command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/root-command.png)\\\\nImportant\\\\nNow you have elevated yourself to the \\\\u0022superuser\\\\u0022 role in your session, you have complete access to the system. Keep in mind that this is a potentially dangerous situation, and you should exercise caution as you continue.\\\\nNow, run the same command one more time:\\\\n\\\\u0060\\\\u0060\\\\u0060bash\\\\necho hello world \\\\u003E /etc/helloworld.txt\\\\n\\\\u0060\\\\u0060\\\\u0060\\\\nThis time, you don\\\\u0027t receive any error message. To verify if the file is created, run the following command:\\\\n\\\\u0060\\\\u0060\\\\u0060bash\\\\nll /etc/hello*\\\\n\\\\u0060\\\\u0060\\\\u0060\\\\nThe \\\\u0060ll\\\\u0060 command lists the files and subfolders that are in a specified folder. In this example, the \\\\u0060/etc/hello*\\\\u0060 parameter value limits the command output to the files or folders whose name begins in \\\\u0060hello\\\\u0060.\\\\n![Screenshot of hello command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/hello-command.png)\\\\nThe output shows that the file was created. What if we want to examine the content of the file? The \\\\u0060cat\\\\u0060 command helps here. To see the content, run \\\\u0060cat /etc/helloworld.txt\\\\u0060.\\\\n![Screenshot of cat command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/cat-command.png)\\\\nThe \\\\u0060cat\\\\u0060 command reads files sequentially, and writes their content to the standard output. Therefore, it writes \\\\u0022hello world\\\\u0022 in the console.\\\\nTo prevent mistakes that could damage the system while you\\\\u0027re elevated to root status, it\\\\u0027s a good practice to exit the root session and return to your standard user session to avoid some dangerous operations. To do this, run \\\\u0060exit\\\\u0060. You can see that the pound sign reverts to a dollar sign, and the username is displayed as your standard user.\\\\n![Screenshot of exit command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/exit-command.png)\\\\nFor additional practice, run the same \\\\u0060ll\\\\u0060 and \\\\u0060cat\\\\u0060 commands within your standard user context. You\\\\u0027ll see that you can\\\\u0027t create the file in the */etc/* folder, but can you list the file and read the contents.\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#run-commands-as-a-superuser\\u0022},{\\u0022title\\u0022:\\u0022Part 1.2 - Basic operations on Linux\\u0022,\\u0022content\\u0022:\\u0022# Part 1.2 - Basic operations on Linux\\\\n## Remove packages\\\\nFinding the file doesn\\\\u0027t involve package managers. Instead, you\\\\u0027ll run a standard search for files in Linux. There are several ways to do so. One of the most common search commands is \\\\u0060find\\\\u0060. However, this command is also confusing to use, so we won\\\\u0027t discuss it here. Instead, we\\\\u0027ll use \\\\u0060whereis\\\\u0060.\\\\nNote\\\\nLinux is very well documented. Nearly every command has a useful help page available. Use the [man](https://www.geeksforgeeks.org/man-command-in-linux-with-examples/) and [info](https://www.geeksforgeeks.org/info-command-in-linux-with-examples/) commands to show the Help pages in Linux. For example, to learn more on your own about the \\\\u0060find\\\\u0060 command, run \\\\u0060man find\\\\u0060 or \\\\u0060info find\\\\u0060.\\\\nAccording to the Help page for \\\\u0060whereis\\\\u0060, the definition is as follows:\\\\n\\\\u0060whereis\\\\u0060 locates the binary, source, and manual files for the specified command names.\\\\nIf you run \\\\u0060whereis apache2\\\\u0060, you should find the */etc/apache2* installation.\\\\n![Screenshot of whereis apache2 command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/whereis-apache2-command.png)\\\\nRemember that the */etc/* directory is where the system configuration files are located. To learn whether \\\\u0022apache2\\\\u0022is a file or folder, run \\\\u0060ll /etc/apache2\\\\u0060. As you can see, this is a folder, and it contains the apache2 configuration files.\\\\n![Screenshot of ll etc apache2 command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/ll-etc-apache2-command.png)\\\\nYou expect \\\\u0060apache2\\\\u0060 to contain these files and folders because you used the \\\\u0060apt remove\\\\u0060 command that doesn\\\\u0027t delete the configuration files.\\\\nAs final step, run the \\\\u0060apt purge\\\\u0060 command to see its effect. You can run a purge to clean up the installation folder even after you remove the package. The following screenshot shows that \\\\u0060whereis\\\\u0060 didn\\\\u0027t find anything. This is because the \\\\u0060purge\\\\u0060 command removed the configuration files together with the binaries.\\\\n![Screenshot of apt purge command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/apt-purge-command.png)\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#remove-packages\\u0022}]}\"}],\"structuredContent\":{\"results\":[{\"title\":\"How to download and install Linux\",\"content\":\"# How to download and install Linux\\n## Step 2 - Choose a Linux distribution\\nThe different versions of Linux are called \\u0022distributions\\u0022 (sometimes shortened to \\u201Cdistros\\u201D). So there isn\\u2019t actually a way to just \\u201Cinstall Linux\\u201D, you first need to choose which distribution that uses the Linux kernel you want to download and install. There are over 600 active Linux distributions, such as Ubuntu, Debian, Kali Linux, openSUSE, and more. There are a number of factors to consider in choosing a distribution, such as:\\n### Required Linux experience\\nIf you\\u2019re new to Linux, you may want to start with Ubuntu. This is one of the most common starting places for Linux users due to the level of support and ease of use. Ubuntu offers a desktop-version and a server-version depending on how you plan to use it. The specific tasks that the operating system will be responsible for may help you determine which distribution to use. Arch Linux is a popular choice for those who want a highly customizable, do-it-yourself approach that is still stable and well-supported by a large user base. It is a much more complicated place to start, but can help you to get a better understanding of how Linux works due to the amount of custom configuration.\\n### System requirements\\nA minimal, or \\u0022lightweight\\u0022, distribution means a small footprint, with lower memory and processing speed requirements. Alpine Linux is a popular option where users will find most features disabled or not installed by default, but still has everything you need for a containerized app. Security needs: Some Linux users may have specific security concerns to consider. Kali Linux is an example of a distro designed for security tasks, such as Penetration Testing, Security Research, Computer Forensics and Reverse Engineering.\\n### Business and Enterprise requirements\\nBusiness, corporate, or academic environments may have unique concerns from consumer environments. For example Red Hat Enterprise Linux (RHEL), SUSE Linux Enterprise Server (SLES) and Oracle Linux offer 24/7 global support and subscription-based service. CentOS is another popular enterprise distro that is community-supported but still compatible with RHEL.\\n### Community support\",\"contentUrl\":\"https://learn.microsoft.com/linux/install#step-2---choose-a-linux-distribution\"},{\"title\":\"Part 1.2 - Basic operations on Linux\",\"content\":\"# Part 1.2 - Basic operations on Linux\\n## Run commands by using the \\u0022sudo\\u0022 prefix\\nInstead of becoming the root account user in your session, you can run commands as root by using the \\u0060sudo \\u003Ccommand\\u003E\\u0060 format. This approach is safer because it will run the given command only as a superuser.\\nFor example, if you run \\u0060restart\\u0060 while you\\u0027re not elevated to superuser status, the result is as follows.\\n![Screenshot of restart command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/restart-command.png)\\nTo run this command as a superuser instead, you can either change the session context to root (by running \\u0060sudo su\\u0060) or add the \\u0060sudo\\u0060 prefix, as follows:\\n\\u0060\\u0060\\u0060bash\\nsudo reboot\\n\\u0060\\u0060\\u0060\\nNow, the restart operation runs as expected by the virtual machine.\\n![Screenshot of reboot command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/reboot-command.png)\\n## Package managers\\nPackage managers are used to install, upgrade, and remove applications in Linux. There are several different package managers, such as DPKG (Debian Package Management System), RPM (Red Hat Package Manager), and Zypper for SUSE Package managers themselves are low-level. Therefore, it might be difficult to manage software directly by using the package manager. Fortunately, there are wrappers for these tools that make it easier to install and uninstall software. For example, [yum](https://wikipedia.org/wiki/Yum_%28software%29) is a wrapper around RPM (Red Hat Package Manager) and [APT](https://wikipedia.org/wiki/APT_%28software%29) is a wrapper around DPKG (Debian Package Management System).\\nThis tutorial assumes that you\\u0027re running the [Ubuntu package management system](https://ubuntu.com/server/docs/package-management). This system is derived from the same system that\\u0027s used by the Debian GNU/Linux distribution. Therefore, we can use APT to install the software.\\nDepending on your Linux choice, you might have to use other tools. For example, you can use yum to install or uninstall software if you\\u0027re running Red Hat.\",\"contentUrl\":\"https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#run-commands-by-using-the-sudo-prefix\"},{\"title\":\"Part 1.2 - Basic operations on Linux\",\"content\":\"# Part 1.2 - Basic operations on Linux\\n## Run commands as a superuser\\nThe superuser is the most privileged user account. It has root (unrestricted) access to all files and folders, and complete control over the operation of the computer. The system administrator uses this account for system maintenance.\\nOn UNIX-like systems, the conventional name of the superuser is \\u0022root\\u0022. The root user can do tasks that are otherwise restricted for standard users, including the following ones:\\n1. Changing system directories\\n2. Changing user privileges\\n3. Changing file ownership\\n4. System-related tasks such as restarts\\n5. Installation of certain applications\\nThere are several ways to run commands as the root account user. One is to use the \\u0060sudo su\\u0060 command. In this command, \\u0060sudo\\u0060 is short for \\u0060superuser do\\u0060, and \\u0060su\\u0060 is an abbreviation for \\u0022switch user.\\u0022 You can also use the \\u0060su\\u0060 command to switch between standard users.\\nImportant\\nAfter you become the root account user, everything you do runs in root context. Therefore, you should act carefully because the root account has access to the entire system.\\nTo demonstrate a permissions issue, create a text file in a special folder that\\u0027s named */etc*.\\nThe */etc/* folder is where system configuration files are located. This folder protects its contents from being changed by a standard user.\\nRun the following command:\\n\\u0060\\u0060\\u0060bash\\necho hello world \\u003E /etc/helloworld.txt\\n\\u0060\\u0060\\u0060\\nThe \\u0060echo\\u0060 command writes all the text that follows it to the output. The angle bracket (\\u0060\\u003E\\u0060) tells the system to send the output to the */etc/helloworld.txt* file instead of the console. This behavior is similar to how Windows works.\\nNote\\nLinux is case-sensitive: *Helloworld.txt*, *helloworld.txt* and *helloworld.Txt* are different files.\\nYou don\\u0027t have to provide a file name extension. The name \\u0022helloworld\\u0022 by itself is perfectly valid. (The \\u0022txt\\u0022 extension is used here only as an example.)\\nWhen you run this command, you receive a **permission denied** error message.\\n![Screenshot of permission denied message.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/permission-denied-message.png)\\nThe operation fails because the */etc* folder is special folder that a standard user can\\u0027t change. Verify the user by running the \\u0060id\\u0060 command.\\n![Screenshot of I D command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/id-command.png)\",\"contentUrl\":\"https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#run-commands-as-a-superuser\"},{\"title\":\"How to download and install Linux\",\"content\":\"# How to download and install Linux\\n## Step 1 - Choose a method to install Linux\\nThe choice of which method to use for installing Linux depends on your needs and preferences.\\n1. **New to using Linux?** We recommend starting with Windows Subsystem for Linux (WSL) as it\\u0027s the easiest way to get up and running.\\n2. **Work in a business environment with more complex needs related to scale or security?** We recommend running Linux as a Virtual Machine (VM) in the cloud and checking out the support that Azure has to offer. This also applies if you want to run Linux as a server.\\n3. **Only want to run Linux as your primary operating system?** If you are good with a slightly more complicated install process and don\\u0027t need access to Windows tools (like Outlook, Teams, Word, PowerPoint, etc), you can run Linux on bare metal to access the full potential of your hardware without any overhead from virtualization or emulation.\\nLearn more about these options below.\\n### Windows Subsystem for Linux (WSL)\\nAvailable to those with a Windows machine, this is the most simple way to install Linux. Just run the Linux install command: \\u0060wsl --install\\u0060 to install the Ubuntu distribution.\\nThis method uses virtualization to integrate the Windows operating system (OS) with the Linux OS (running on an actual Linux kernel). You can add as many different Linux distributions as you please and run your favorite Linux tools, including GUI apps, alongside your favorite Windows tools. You can also mix and match Bash and PowerShell commands in the same command line. A Linux file system hosts your project files, while a separate Windows file systems hosts any developer tools, such as VS Code, that you\\u0027d like to work on the files with. This method of running Linux is highly efficient and performant. Once you choose a Linux distribution or opt to use the default Ubuntu distro, follow the instructions to Use the install Linux command with Windows Subsystem for Linux or learn more in the [WSL documentation](https://learn.microsoft.com/en-us/windows/wsl).\\n![WSL install command screenshot](https://learn.microsoft.com/linux/images/wsl-install-command-screenshot.png)\\n### Create a Virtual Machine (VM)\\nAvailable to everyone regardless if you\\u0027re using a Windows or MacOS-based machine, but will require either an Azure account, if creating a VM in the cloud, or virtualization software, if creating a VM directly on your machine (locally).\",\"contentUrl\":\"https://learn.microsoft.com/linux/install#step-1---choose-a-method-to-install-linux\"},{\"title\":\"How to download and install Linux\",\"content\":\"# How to download and install Linux\\nLinux is an operating system, similar to Windows, but with many different versions due to the nature of being open source and fully customizable. To install Linux, you must choose an install method and choose a Linux distribution.\\nTo install Linux:\\n1. Choose an install method: Windows Subsystem for Linux (WSL), Bare metal Linux; or create a Virtual Machine (VM) to run Linux locally or in the cloud.\\n2. Choose a Linux distribution: Ubuntu, Debian, Kali Linux, openSUSE, etc.\\n3. Follow the steps for your preferred install method:\\n3.1. Use the install Linux command with Windows Subsystem for Linux (WSL)\\n3.2. Create a Linux Virtual Machine (VM) in the cloud\\n3.3. Create a Linux Virtual Machine (VM) on your local machine\\n3.4. Create a bootable USB to install bare-metal Linux\\n4. After installing Linux: Get familiar with your distribution\\u0027s package manager, update and upgrade the packages available, and get familiar with the other [Linux resources at Microsoft](https://learn.microsoft.com/linux/), such as training courses, Linux-versions of popular tools, news, and Open Source events.\",\"contentUrl\":\"https://learn.microsoft.com/linux/install\"},{\"title\":\"New to Linux resources for SQL users\",\"content\":\"# New to Linux resources for SQL users\\n**Applies to:**![](https://learn.microsoft.com/sql/includes/media/yes-icon.svg?view=sql-server-ver17)[SQL Server](https://learn.microsoft.com/sql/sql-server/sql-docs-navigation-guide?view=sql-server-ver17#applies-to) on Linux\\nThis article provides a learning path through an introduction to Linux concepts. Use the sections in the article as a guided learning path to familiarize yourself with the Linux environment.\\nThis list isn\\u0027t comprehensive but instead provides the bare minimum you need to help you administer and navigate your SQL Server on Linux environment. See the [full list of tutorials](https://www.linux.org/forums/linux-beginner-tutorials.123/) to learn more.\\n## What is Linux?\\nThe [What is Linux](https://www.linux.org/threads/what-is-linux.4106/) module introduces the history of Linux as an operating system. The module explains *the Kernel* and where Linux is today. This tutorial serves as an introduction to Linux, helping you get started.\\n## Select a distribution\\nAfter you know the history of Linux, decide which [distribution of Linux](https://www.linux.org/threads/selecting-a-linux-distribution.4117/) best suits your business needs.\\nSQL Server 2025 (17.x) is [supported](https://learn.microsoft.com/sql/linux/sql-server-linux-release-notes-2025?view=sql-server-ver17#supported-platforms) on Red Hat Enterprise Linux (RHEL) and Ubuntu.\\nSQL Server 2022 (16.x) and earlier versions are [also supported](https://learn.microsoft.com/sql/linux/sql-server-linux-release-notes-2022?view=sql-server-ver17#supported-platforms) on SUSE Linux Enterprise Server (SLES).\\n## Get around directories\\nAfter you choose the appropriate distribution of Linux, familiarize yourself with [getting around Linux directories](https://www.linux.org/threads/getting-around-in-linux-directories.4120/).\\nThis module helps you answer the following questions:\\n1. How to navigate between different files\\n2. How to identify a file from a directory\\n3. Identify different directories\\n## Install new software\\nNext, learn how to [install new software](https://www.linux.org/threads/installing-new-software-debian-red-hat-slackware.4119/) on your new Linux operating system. This module covers installing new software on the Debian and Red Hat Linux operating systems.\",\"contentUrl\":\"https://learn.microsoft.com/sql/linux/new-to-linux-resources?view=sql-server-ver17\"},{\"title\":\"How to download and install Linux\",\"content\":\"# How to download and install Linux\\n## Step 3 - Follow install method instructions\\n### Install Linux with Windows Subsystem for Linux\\nTo install Linux on a Windows PC, use the install Linux command.\\n1. Open PowerShell or Windows Command Prompt in *administrator mode* by right-clicking and selecting \\u0022Run as administrator\\u0022.\\n2. Enter the Linux install command: \\u0060wsl --install\\u0060.\\n3. Restart your machine.\\n![WSL Linux Install List Command screenshot](https://learn.microsoft.com/linux/images/wsl-install-list-screenshot.png)\\n#### Install Linux command options\\nBy default, the install command will use the Ubuntu distribution of Linux. You can see the other distributions that are available in the Microsoft Store by entering the command: \\u0060wsl -l -o\\u0060. If you\\u0027d like to install a distribution other than Ubuntu, just add \\u0060-d\\u0060 to the command with the name of the distribution you\\u0027d like to install:\\n\\u0060\\u0060\\u0060powershell\\nwsl --install -d \\u003CDistro Name\\u003E\\n\\u0060\\u0060\\u0060\\nIf the distribution you want to install isn\\u0027t available in the Microsoft Store, see the Windows Subsystem for Linux (WSL) documentation on how to [Import any Linux distribution to use with WSL](https://learn.microsoft.com/en-us/windows/wsl/use-custom-distro).\\nFor more information on how to use VS Code, Git, databases, Docker, GUI apps, GPU acceleration, NodesJS, USB devices, or mount a disk, build a custom distribution, manage disk space, or set up WSL for your enterprise business, see the [WSL documentation](https://learn.microsoft.com/en-us/windows/wsl/).\\n### Create a Linux Virtual Machine in the cloud\\nTo create a Linux Virtual Machine (VM) that is hosted in the cloud on Azure, log-in to the Azure Portal and select the \\u201CCreate a Virtual Machine\\u201D service. Then follow the instructions for creating a user name, project, instance, SSH key, ports, and copying the IP address.\\nAzure supports a variety of options for setting up Linux VMs in the cloud. For a quickstart guide on how to set up a Linux VM, choose your preferred method:\\n1. [Azure Portal](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-portal?tabs=ubuntu\\u0026source=docs)\\n2. [Azure CLI](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-cli)\\n3. [Azure PowerShell](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-powershell)\\n4. [Terraform](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-terraform)\",\"contentUrl\":\"https://learn.microsoft.com/linux/install#step-3---follow-install-method-instructions\"},{\"title\":\"How to download and install Linux\",\"content\":\"# How to download and install Linux\\n## Step 2 - Choose a Linux distribution\\nA large user community that consistently contributes to the distribution, fixing bugs, adding features, helping with questions, etc. is another good consideration. Ubuntu is one of the most popular distos. openSUSE has been around longer than most other distributions and offers in addition to the stable version (Leap) a rolling version (Tumbleweed). Many other distros rapidly grow popular as the needs and preferences of the user community change. The Linux.org site has resources to help you assess what\\u0027s available, well-supported, or growing in popularity.\\n![Screenshot of the list of distributions on Linux.org](https://learn.microsoft.com/linux/images/linux-distributions-list-screenshot.png)\",\"contentUrl\":\"https://learn.microsoft.com/linux/install#step-2---choose-a-linux-distribution\"},{\"title\":\"Part 1.2 - Basic operations on Linux\",\"content\":\"# Part 1.2 - Basic operations on Linux\\n## Run commands as a superuser\\nTo become the root account user, run the \\u0060sudo su\\u0060 command.\\n![Screenshot of sudo su command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/sudo-su-command.png)\\nYou should notice two things: When you become root, the dollar sign character (\\u0060$\\u0060) becomes a pound sign character (\\u0060#\\u0060). Also, the username is changed to **root**. If you run the same \\u0060id\\u0060 command again, you see that the user and group ID of root are **0**.\\n![Screenshot of root command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/root-command.png)\\nImportant\\nNow you have elevated yourself to the \\u0022superuser\\u0022 role in your session, you have complete access to the system. Keep in mind that this is a potentially dangerous situation, and you should exercise caution as you continue.\\nNow, run the same command one more time:\\n\\u0060\\u0060\\u0060bash\\necho hello world \\u003E /etc/helloworld.txt\\n\\u0060\\u0060\\u0060\\nThis time, you don\\u0027t receive any error message. To verify if the file is created, run the following command:\\n\\u0060\\u0060\\u0060bash\\nll /etc/hello*\\n\\u0060\\u0060\\u0060\\nThe \\u0060ll\\u0060 command lists the files and subfolders that are in a specified folder. In this example, the \\u0060/etc/hello*\\u0060 parameter value limits the command output to the files or folders whose name begins in \\u0060hello\\u0060.\\n![Screenshot of hello command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/hello-command.png)\\nThe output shows that the file was created. What if we want to examine the content of the file? The \\u0060cat\\u0060 command helps here. To see the content, run \\u0060cat /etc/helloworld.txt\\u0060.\\n![Screenshot of cat command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/cat-command.png)\\nThe \\u0060cat\\u0060 command reads files sequentially, and writes their content to the standard output. Therefore, it writes \\u0022hello world\\u0022 in the console.\\nTo prevent mistakes that could damage the system while you\\u0027re elevated to root status, it\\u0027s a good practice to exit the root session and return to your standard user session to avoid some dangerous operations. To do this, run \\u0060exit\\u0060. You can see that the pound sign reverts to a dollar sign, and the username is displayed as your standard user.\\n![Screenshot of exit command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/exit-command.png)\\nFor additional practice, run the same \\u0060ll\\u0060 and \\u0060cat\\u0060 commands within your standard user context. You\\u0027ll see that you can\\u0027t create the file in the */etc/* folder, but can you list the file and read the contents.\",\"contentUrl\":\"https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#run-commands-as-a-superuser\"},{\"title\":\"Part 1.2 - Basic operations on Linux\",\"content\":\"# Part 1.2 - Basic operations on Linux\\n## Remove packages\\nFinding the file doesn\\u0027t involve package managers. Instead, you\\u0027ll run a standard search for files in Linux. There are several ways to do so. One of the most common search commands is \\u0060find\\u0060. However, this command is also confusing to use, so we won\\u0027t discuss it here. Instead, we\\u0027ll use \\u0060whereis\\u0060.\\nNote\\nLinux is very well documented. Nearly every command has a useful help page available. Use the [man](https://www.geeksforgeeks.org/man-command-in-linux-with-examples/) and [info](https://www.geeksforgeeks.org/info-command-in-linux-with-examples/) commands to show the Help pages in Linux. For example, to learn more on your own about the \\u0060find\\u0060 command, run \\u0060man find\\u0060 or \\u0060info find\\u0060.\\nAccording to the Help page for \\u0060whereis\\u0060, the definition is as follows:\\n\\u0060whereis\\u0060 locates the binary, source, and manual files for the specified command names.\\nIf you run \\u0060whereis apache2\\u0060, you should find the */etc/apache2* installation.\\n![Screenshot of whereis apache2 command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/whereis-apache2-command.png)\\nRemember that the */etc/* directory is where the system configuration files are located. To learn whether \\u0022apache2\\u0022is a file or folder, run \\u0060ll /etc/apache2\\u0060. As you can see, this is a folder, and it contains the apache2 configuration files.\\n![Screenshot of ll etc apache2 command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/ll-etc-apache2-command.png)\\nYou expect \\u0060apache2\\u0060 to contain these files and folders because you used the \\u0060apt remove\\u0060 command that doesn\\u0027t delete the configuration files.\\nAs final step, run the \\u0060apt purge\\u0060 command to see its effect. You can run a purge to clean up the installation folder even after you remove the package. The following screenshot shows that \\u0060whereis\\u0060 didn\\u0027t find anything. This is because the \\u0060purge\\u0060 command removed the configuration files together with the binaries.\\n![Screenshot of apt purge command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/apt-purge-command.png)\",\"contentUrl\":\"https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#remove-packages\"}]}},\"id\":\"07e26d19ee6248d98f6dc039bf4fc589\",\"jsonrpc\":\"2.0\"}\n",
+       "\n",
+       "\n",
+       "BODY END
" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Title: \"How to download and install Linux\"\n", + "\n", + "Content: \"# How to download and install Linux\\n## Step 2 - Choose a Linux distribution\\nThe different versions of Linux are called \\\"distributions\\\" (sometimes shortened to “distros”). So there isn’t actually a way to just “install Linux”, you first need to choose which distribution that uses the Linux kernel you want to download and install. There are over 600 active Linux distributions, such as Ubuntu, Debian, Kali Linux, openSUSE, and more. There are a number of factors to consider in choosing a distribution, such as:\\n### Required Linux experience\\nIf you’re new to Linux, you may want to start with Ubuntu. This is one of the most common starting places for Linux users due to the level of support and ease of use. Ubuntu offers a desktop-version and a server-version depending on how you plan to use it. The specific tasks that the operating system will be responsible for may help you determine which distribution to use. Arch Linux is a popular choice for those who want a highly customizable, do-it-yourself approach that is still stable and well-supported by a large user base. It is a much more complicated place to start, but can help you to get a better understanding of how Linux works due to the amount of custom configuration.\\n### System requirements\\nA minimal, or \\\"lightweight\\\", distribution means a small footprint, with lower memory and processing speed requirements. Alpine Linux is a popular option where users will find most features disabled or not installed by default, but still has everything you need for a containerized app. Security needs: Some Linux users may have specific security concerns to consider. Kali Linux is an example of a distro designed for security tasks, such as Penetration Testing, Security Research, Computer Forensics and Reverse Engineering.\\n### Business and Enterprise requirements\\nBusiness, corporate, or academic environments may have unique concerns from consumer environments. For example Red Hat Enterprise Linux (RHEL), SUSE Linux Enterprise Server (SLES) and Oracle Linux offer 24/7 global support and subscription-based service. CentOS is another popular enterprise distro that is community-supported but still compatible with RHEL.\\n### Community support\"\n", + "Content URL: \"https://learn.microsoft.com/linux/install#step-2---choose-a-linux-distribution\"\n", + "Title: \"Part 1.2 - Basic operations on Linux\"\n", + "\n", + "Content: \"# Part 1.2 - Basic operations on Linux\\n## Run commands by using the \\\"sudo\\\" prefix\\nInstead of becoming the root account user in your session, you can run commands as root by using the `sudo ` format. This approach is safer because it will run the given command only as a superuser.\\nFor example, if you run `restart` while you're not elevated to superuser status, the result is as follows.\\n![Screenshot of restart command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/restart-command.png)\\nTo run this command as a superuser instead, you can either change the session context to root (by running `sudo su`) or add the `sudo` prefix, as follows:\\n```bash\\nsudo reboot\\n```\\nNow, the restart operation runs as expected by the virtual machine.\\n![Screenshot of reboot command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/reboot-command.png)\\n## Package managers\\nPackage managers are used to install, upgrade, and remove applications in Linux. There are several different package managers, such as DPKG (Debian Package Management System), RPM (Red Hat Package Manager), and Zypper for SUSE Package managers themselves are low-level. Therefore, it might be difficult to manage software directly by using the package manager. Fortunately, there are wrappers for these tools that make it easier to install and uninstall software. For example, [yum](https://wikipedia.org/wiki/Yum_%28software%29) is a wrapper around RPM (Red Hat Package Manager) and [APT](https://wikipedia.org/wiki/APT_%28software%29) is a wrapper around DPKG (Debian Package Management System).\\nThis tutorial assumes that you're running the [Ubuntu package management system](https://ubuntu.com/server/docs/package-management). This system is derived from the same system that's used by the Debian GNU/Linux distribution. Therefore, we can use APT to install the software.\\nDepending on your Linux choice, you might have to use other tools. For example, you can use yum to install or uninstall software if you're running Red Hat.\"\n", + "Content URL: \"https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#run-commands-by-using-the-sudo-prefix\"\n", + "Title: \"Part 1.2 - Basic operations on Linux\"\n", + "\n", + "Content: \"# Part 1.2 - Basic operations on Linux\\n## Run commands as a superuser\\nThe superuser is the most privileged user account. It has root (unrestricted) access to all files and folders, and complete control over the operation of the computer. The system administrator uses this account for system maintenance.\\nOn UNIX-like systems, the conventional name of the superuser is \\\"root\\\". The root user can do tasks that are otherwise restricted for standard users, including the following ones:\\n1. Changing system directories\\n2. Changing user privileges\\n3. Changing file ownership\\n4. System-related tasks such as restarts\\n5. Installation of certain applications\\nThere are several ways to run commands as the root account user. One is to use the `sudo su` command. In this command, `sudo` is short for `superuser do`, and `su` is an abbreviation for \\\"switch user.\\\" You can also use the `su` command to switch between standard users.\\nImportant\\nAfter you become the root account user, everything you do runs in root context. Therefore, you should act carefully because the root account has access to the entire system.\\nTo demonstrate a permissions issue, create a text file in a special folder that's named */etc*.\\nThe */etc/* folder is where system configuration files are located. This folder protects its contents from being changed by a standard user.\\nRun the following command:\\n```bash\\necho hello world > /etc/helloworld.txt\\n```\\nThe `echo` command writes all the text that follows it to the output. The angle bracket (`>`) tells the system to send the output to the */etc/helloworld.txt* file instead of the console. This behavior is similar to how Windows works.\\nNote\\nLinux is case-sensitive: *Helloworld.txt*, *helloworld.txt* and *helloworld.Txt* are different files.\\nYou don't have to provide a file name extension. The name \\\"helloworld\\\" by itself is perfectly valid. (The \\\"txt\\\" extension is used here only as an example.)\\nWhen you run this command, you receive a **permission denied** error message.\\n![Screenshot of permission denied message.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/permission-denied-message.png)\\nThe operation fails because the */etc* folder is special folder that a standard user can't change. Verify the user by running the `id` command.\\n![Screenshot of I D command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/id-command.png)\"\n", + "Content URL: \"https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#run-commands-as-a-superuser\"\n", + "Title: \"How to download and install Linux\"\n", + "\n", + "Content: \"# How to download and install Linux\\n## Step 1 - Choose a method to install Linux\\nThe choice of which method to use for installing Linux depends on your needs and preferences.\\n1. **New to using Linux?** We recommend starting with Windows Subsystem for Linux (WSL) as it's the easiest way to get up and running.\\n2. **Work in a business environment with more complex needs related to scale or security?** We recommend running Linux as a Virtual Machine (VM) in the cloud and checking out the support that Azure has to offer. This also applies if you want to run Linux as a server.\\n3. **Only want to run Linux as your primary operating system?** If you are good with a slightly more complicated install process and don't need access to Windows tools (like Outlook, Teams, Word, PowerPoint, etc), you can run Linux on bare metal to access the full potential of your hardware without any overhead from virtualization or emulation.\\nLearn more about these options below.\\n### Windows Subsystem for Linux (WSL)\\nAvailable to those with a Windows machine, this is the most simple way to install Linux. Just run the Linux install command: `wsl --install` to install the Ubuntu distribution.\\nThis method uses virtualization to integrate the Windows operating system (OS) with the Linux OS (running on an actual Linux kernel). You can add as many different Linux distributions as you please and run your favorite Linux tools, including GUI apps, alongside your favorite Windows tools. You can also mix and match Bash and PowerShell commands in the same command line. A Linux file system hosts your project files, while a separate Windows file systems hosts any developer tools, such as VS Code, that you'd like to work on the files with. This method of running Linux is highly efficient and performant. Once you choose a Linux distribution or opt to use the default Ubuntu distro, follow the instructions to Use the install Linux command with Windows Subsystem for Linux or learn more in the [WSL documentation](https://learn.microsoft.com/en-us/windows/wsl).\\n![WSL install command screenshot](https://learn.microsoft.com/linux/images/wsl-install-command-screenshot.png)\\n### Create a Virtual Machine (VM)\\nAvailable to everyone regardless if you're using a Windows or MacOS-based machine, but will require either an Azure account, if creating a VM in the cloud, or virtualization software, if creating a VM directly on your machine (locally).\"\n", + "Content URL: \"https://learn.microsoft.com/linux/install#step-1---choose-a-method-to-install-linux\"\n", + "Title: \"How to download and install Linux\"\n", + "\n", + "Content: \"# How to download and install Linux\\nLinux is an operating system, similar to Windows, but with many different versions due to the nature of being open source and fully customizable. To install Linux, you must choose an install method and choose a Linux distribution.\\nTo install Linux:\\n1. Choose an install method: Windows Subsystem for Linux (WSL), Bare metal Linux; or create a Virtual Machine (VM) to run Linux locally or in the cloud.\\n2. Choose a Linux distribution: Ubuntu, Debian, Kali Linux, openSUSE, etc.\\n3. Follow the steps for your preferred install method:\\n3.1. Use the install Linux command with Windows Subsystem for Linux (WSL)\\n3.2. Create a Linux Virtual Machine (VM) in the cloud\\n3.3. Create a Linux Virtual Machine (VM) on your local machine\\n3.4. Create a bootable USB to install bare-metal Linux\\n4. After installing Linux: Get familiar with your distribution's package manager, update and upgrade the packages available, and get familiar with the other [Linux resources at Microsoft](https://learn.microsoft.com/linux/), such as training courses, Linux-versions of popular tools, news, and Open Source events.\"\n", + "Content URL: \"https://learn.microsoft.com/linux/install\"\n", + "Title: \"New to Linux resources for SQL users\"\n", + "\n", + "Content: \"# New to Linux resources for SQL users\\n**Applies to:**![](https://learn.microsoft.com/sql/includes/media/yes-icon.svg?view=sql-server-ver17)[SQL Server](https://learn.microsoft.com/sql/sql-server/sql-docs-navigation-guide?view=sql-server-ver17#applies-to) on Linux\\nThis article provides a learning path through an introduction to Linux concepts. Use the sections in the article as a guided learning path to familiarize yourself with the Linux environment.\\nThis list isn't comprehensive but instead provides the bare minimum you need to help you administer and navigate your SQL Server on Linux environment. See the [full list of tutorials](https://www.linux.org/forums/linux-beginner-tutorials.123/) to learn more.\\n## What is Linux?\\nThe [What is Linux](https://www.linux.org/threads/what-is-linux.4106/) module introduces the history of Linux as an operating system. The module explains *the Kernel* and where Linux is today. This tutorial serves as an introduction to Linux, helping you get started.\\n## Select a distribution\\nAfter you know the history of Linux, decide which [distribution of Linux](https://www.linux.org/threads/selecting-a-linux-distribution.4117/) best suits your business needs.\\nSQL Server 2025 (17.x) is [supported](https://learn.microsoft.com/sql/linux/sql-server-linux-release-notes-2025?view=sql-server-ver17#supported-platforms) on Red Hat Enterprise Linux (RHEL) and Ubuntu.\\nSQL Server 2022 (16.x) and earlier versions are [also supported](https://learn.microsoft.com/sql/linux/sql-server-linux-release-notes-2022?view=sql-server-ver17#supported-platforms) on SUSE Linux Enterprise Server (SLES).\\n## Get around directories\\nAfter you choose the appropriate distribution of Linux, familiarize yourself with [getting around Linux directories](https://www.linux.org/threads/getting-around-in-linux-directories.4120/).\\nThis module helps you answer the following questions:\\n1. How to navigate between different files\\n2. How to identify a file from a directory\\n3. Identify different directories\\n## Install new software\\nNext, learn how to [install new software](https://www.linux.org/threads/installing-new-software-debian-red-hat-slackware.4119/) on your new Linux operating system. This module covers installing new software on the Debian and Red Hat Linux operating systems.\"\n", + "Content URL: \"https://learn.microsoft.com/sql/linux/new-to-linux-resources?view=sql-server-ver17\"\n", + "Title: \"How to download and install Linux\"\n", + "\n", + "Content: \"# How to download and install Linux\\n## Step 3 - Follow install method instructions\\n### Install Linux with Windows Subsystem for Linux\\nTo install Linux on a Windows PC, use the install Linux command.\\n1. Open PowerShell or Windows Command Prompt in *administrator mode* by right-clicking and selecting \\\"Run as administrator\\\".\\n2. Enter the Linux install command: `wsl --install`.\\n3. Restart your machine.\\n![WSL Linux Install List Command screenshot](https://learn.microsoft.com/linux/images/wsl-install-list-screenshot.png)\\n#### Install Linux command options\\nBy default, the install command will use the Ubuntu distribution of Linux. You can see the other distributions that are available in the Microsoft Store by entering the command: `wsl -l -o`. If you'd like to install a distribution other than Ubuntu, just add `-d` to the command with the name of the distribution you'd like to install:\\n```powershell\\nwsl --install -d \\n```\\nIf the distribution you want to install isn't available in the Microsoft Store, see the Windows Subsystem for Linux (WSL) documentation on how to [Import any Linux distribution to use with WSL](https://learn.microsoft.com/en-us/windows/wsl/use-custom-distro).\\nFor more information on how to use VS Code, Git, databases, Docker, GUI apps, GPU acceleration, NodesJS, USB devices, or mount a disk, build a custom distribution, manage disk space, or set up WSL for your enterprise business, see the [WSL documentation](https://learn.microsoft.com/en-us/windows/wsl/).\\n### Create a Linux Virtual Machine in the cloud\\nTo create a Linux Virtual Machine (VM) that is hosted in the cloud on Azure, log-in to the Azure Portal and select the “Create a Virtual Machine” service. Then follow the instructions for creating a user name, project, instance, SSH key, ports, and copying the IP address.\\nAzure supports a variety of options for setting up Linux VMs in the cloud. For a quickstart guide on how to set up a Linux VM, choose your preferred method:\\n1. [Azure Portal](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-portal?tabs=ubuntu&source=docs)\\n2. [Azure CLI](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-cli)\\n3. [Azure PowerShell](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-powershell)\\n4. [Terraform](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-terraform)\"\n", + "Content URL: \"https://learn.microsoft.com/linux/install#step-3---follow-install-method-instructions\"\n", + "Title: \"How to download and install Linux\"\n", + "\n", + "Content: \"# How to download and install Linux\\n## Step 2 - Choose a Linux distribution\\nA large user community that consistently contributes to the distribution, fixing bugs, adding features, helping with questions, etc. is another good consideration. Ubuntu is one of the most popular distos. openSUSE has been around longer than most other distributions and offers in addition to the stable version (Leap) a rolling version (Tumbleweed). Many other distros rapidly grow popular as the needs and preferences of the user community change. The Linux.org site has resources to help you assess what's available, well-supported, or growing in popularity.\\n![Screenshot of the list of distributions on Linux.org](https://learn.microsoft.com/linux/images/linux-distributions-list-screenshot.png)\"\n", + "Content URL: \"https://learn.microsoft.com/linux/install#step-2---choose-a-linux-distribution\"\n", + "Title: \"Part 1.2 - Basic operations on Linux\"\n", + "\n", + "Content: \"# Part 1.2 - Basic operations on Linux\\n## Run commands as a superuser\\nTo become the root account user, run the `sudo su` command.\\n![Screenshot of sudo su command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/sudo-su-command.png)\\nYou should notice two things: When you become root, the dollar sign character (`$`) becomes a pound sign character (`#`). Also, the username is changed to **root**. If you run the same `id` command again, you see that the user and group ID of root are **0**.\\n![Screenshot of root command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/root-command.png)\\nImportant\\nNow you have elevated yourself to the \\\"superuser\\\" role in your session, you have complete access to the system. Keep in mind that this is a potentially dangerous situation, and you should exercise caution as you continue.\\nNow, run the same command one more time:\\n```bash\\necho hello world > /etc/helloworld.txt\\n```\\nThis time, you don't receive any error message. To verify if the file is created, run the following command:\\n```bash\\nll /etc/hello*\\n```\\nThe `ll` command lists the files and subfolders that are in a specified folder. In this example, the `/etc/hello*` parameter value limits the command output to the files or folders whose name begins in `hello`.\\n![Screenshot of hello command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/hello-command.png)\\nThe output shows that the file was created. What if we want to examine the content of the file? The `cat` command helps here. To see the content, run `cat /etc/helloworld.txt`.\\n![Screenshot of cat command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/cat-command.png)\\nThe `cat` command reads files sequentially, and writes their content to the standard output. Therefore, it writes \\\"hello world\\\" in the console.\\nTo prevent mistakes that could damage the system while you're elevated to root status, it's a good practice to exit the root session and return to your standard user session to avoid some dangerous operations. To do this, run `exit`. You can see that the pound sign reverts to a dollar sign, and the username is displayed as your standard user.\\n![Screenshot of exit command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/exit-command.png)\\nFor additional practice, run the same `ll` and `cat` commands within your standard user context. You'll see that you can't create the file in the */etc/* folder, but can you list the file and read the contents.\"\n", + "Content URL: \"https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#run-commands-as-a-superuser\"\n", + "Title: \"Part 1.2 - Basic operations on Linux\"\n", + "\n", + "Content: \"# Part 1.2 - Basic operations on Linux\\n## Remove packages\\nFinding the file doesn't involve package managers. Instead, you'll run a standard search for files in Linux. There are several ways to do so. One of the most common search commands is `find`. However, this command is also confusing to use, so we won't discuss it here. Instead, we'll use `whereis`.\\nNote\\nLinux is very well documented. Nearly every command has a useful help page available. Use the [man](https://www.geeksforgeeks.org/man-command-in-linux-with-examples/) and [info](https://www.geeksforgeeks.org/info-command-in-linux-with-examples/) commands to show the Help pages in Linux. For example, to learn more on your own about the `find` command, run `man find` or `info find`.\\nAccording to the Help page for `whereis`, the definition is as follows:\\n`whereis` locates the binary, source, and manual files for the specified command names.\\nIf you run `whereis apache2`, you should find the */etc/apache2* installation.\\n![Screenshot of whereis apache2 command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/whereis-apache2-command.png)\\nRemember that the */etc/* directory is where the system configuration files are located. To learn whether \\\"apache2\\\"is a file or folder, run `ll /etc/apache2`. As you can see, this is a folder, and it contains the apache2 configuration files.\\n![Screenshot of ll etc apache2 command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/ll-etc-apache2-command.png)\\nYou expect `apache2` to contain these files and folders because you used the `apt remove` command that doesn't delete the configuration files.\\nAs final step, run the `apt purge` command to see its effect. You can run a purge to clean up the installation folder even after you remove the package. The following screenshot shows that `whereis` didn't find anything. This is because the `purge` command removed the configuration files together with the binaries.\\n![Screenshot of apt purge command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/apt-purge-command.png)\"\n", + "Content URL: \"https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#remove-packages\"\n" + ] + } + ], + "execution_count": 14 + } + ], + "metadata": { + "kernelspec": { + "display_name": "Kotlin", + "language": "kotlin", + "name": "kotlin" + }, + "language_info": { + "name": "kotlin", + "version": "2.2.20-Beta2", + "mimetype": "text/x-kotlin", + "file_extension": ".kt", + "pygments_lexer": "kotlin", + "codemirror_mode": "text/x-kotlin", + "nbconvert_exporter": "" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/samples/notebooks/README.md b/samples/notebooks/README.md new file mode 100644 index 0000000..8db4f3b --- /dev/null +++ b/samples/notebooks/README.md @@ -0,0 +1,31 @@ +# MCP Client Notebook + +An interactive Kotlin notebook that demonstrates connecting to a remote MCP server and calling tools. + +## Overview + +This notebook walks through building an MCP client step by step: creating a Ktor HTTP client, +initializing an MCP `Client`, connecting via `StreamableHttpClientTransport`, and interacting with +the server (ping, list tools, call tools). It connects to the public +[Microsoft Learn MCP Server](https://learn.microsoft.com/api/mcp) as an example. + +## Prerequisites + +- [Kotlin Jupyter kernel](https://github.com/Kotlin/kotlin-jupyter) **or** IntelliJ IDEA with the + [Kotlin Notebook plugin](https://plugins.jetbrains.com/plugin/16340-kotlin-notebook) +- Internet access (the notebook connects to an external MCP server) + +## How to Run + +1. Open `McpClient.ipynb` in IntelliJ IDEA (with Kotlin Notebook plugin) or in Jupyter with the + Kotlin kernel installed. +2. Run cells sequentially from top to bottom. + +## What It Demonstrates + +- Adding MCP SDK dependencies in a notebook environment +- Creating a Ktor `HttpClient` with SSE and logging plugins +- Creating an MCP `Client` and connecting via `StreamableHttpClientTransport` +- Sending a `ping` request +- Listing available tools from the remote server +- Calling a tool (`microsoft_docs_search`) and displaying results \ No newline at end of file diff --git a/samples/simple-streamable-server/README.md b/samples/simple-streamable-server/README.md new file mode 100644 index 0000000..27856d7 --- /dev/null +++ b/samples/simple-streamable-server/README.md @@ -0,0 +1,75 @@ +# Simple Streamable HTTP Server + +A minimal MCP server using the recommended Streamable HTTP transport with optional Bearer token authentication. + +## Overview + +This sample demonstrates a Streamable HTTP MCP server built with Ktor. It exposes tools, a prompt +template, and a resource over HTTP, and optionally supports Bearer token authentication. The +`multi-greet` tool showcases server-to-client logging notifications with streaming delays. + +## Prerequisites + +- JDK 17+ + +## Build & Run + +### Without authentication + +```shell +./gradlew run +``` + +The server starts on `http://localhost:3001/mcp` by default. + +Connect with the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector): + +```shell +npx @modelcontextprotocol/inspector +``` + +In the Inspector UI, select **Streamable HTTP** transport and enter `http://localhost:3001/mcp`. + +Pass a port number as an argument to change it: + +```shell +./gradlew run --args="8080" +``` + +### With authentication + +```shell +MCP_AUTH_TOKEN=my-secret ./gradlew run --args="--auth" +``` + +When `--auth` is passed, clients must include an `Authorization: Bearer ` header. +The token is read from the `MCP_AUTH_TOKEN` environment variable (required when `--auth` is used). + +### Authentication caveats + +This sample is intended **for demonstration only**. In production you should: + +- Use a proper identity provider instead of a static token. +- Restrict CORS origins to your deployment domain — the sample allows all origins. +- Serve the endpoint over HTTPS. + +## MCP Capabilities + +### Tools + +| Name | Description | +|------|-------------| +| `greet` | Returns a greeting for a given `name`. | +| `multi-greet` | Sends logging notifications between delayed greetings, demonstrating streaming. | + +### Prompts + +| Name | Description | +|------|-------------| +| `greeting-template` | Generates a friendly greeting message for a given `name`. | + +### Resources + +| Name | URI | Description | +|------|-----|-------------| +| `Default Greeting` | `https://example.com/greetings/default` | Returns a static "Hello, world!" text. | \ No newline at end of file diff --git a/samples/simple-streamable-server/build.gradle.kts b/samples/simple-streamable-server/build.gradle.kts new file mode 100644 index 0000000..0296429 --- /dev/null +++ b/samples/simple-streamable-server/build.gradle.kts @@ -0,0 +1,38 @@ +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.shadow) + application +} + +group = "org.example" +version = "0.1.0" + +application { + mainClass.set("io.modelcontextprotocol.sample.server.MainKt") +} + +dependencies { + implementation(dependencies.platform(libs.ktor.bom)) + implementation(libs.mcp.kotlin.server) + implementation(libs.ktor.server.netty) + implementation(libs.ktor.server.auth) + implementation(libs.ktor.server.cors) + implementation(libs.ktor.server.content.negotiation) + implementation(libs.ktor.server.sse) + implementation(libs.ktor.serialization.kotlinx.json) + implementation(libs.slf4j.simple) + + testImplementation(libs.mcp.kotlin.client) + testImplementation(libs.ktor.client.cio) + testImplementation(libs.junit.jupiter) + testImplementation(kotlin("test")) +} + +tasks.test { + useJUnitPlatform() +} + +kotlin { + jvmToolchain(17) +} diff --git a/samples/simple-streamable-server/gradle.properties b/samples/simple-streamable-server/gradle.properties new file mode 100644 index 0000000..32cead6 --- /dev/null +++ b/samples/simple-streamable-server/gradle.properties @@ -0,0 +1,5 @@ +org.gradle.configuration-cache=true +org.gradle.parallel=true +org.gradle.caching=true + +#mcp.kotlin.overrideVersion=0.0.1-SNAPSHOT diff --git a/samples/simple-streamable-server/gradle/libs.versions.toml b/samples/simple-streamable-server/gradle/libs.versions.toml new file mode 100644 index 0000000..5f35f74 --- /dev/null +++ b/samples/simple-streamable-server/gradle/libs.versions.toml @@ -0,0 +1,26 @@ +[versions] +kotlin = "2.4.20" +ktor = "3.5.2" +junit = "6.1.3" +mcp-kotlin = "0.15.0" +slf4j = "2.0.19" +shadow = "9.6.1" + +[libraries] +ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" } +ktor-server-netty = { group = "io.ktor", name = "ktor-server-netty" } +ktor-server-cors = { group = "io.ktor", name = "ktor-server-cors" } +ktor-server-content-negotiation = { group = "io.ktor", name = "ktor-server-content-negotiation" } +ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json" } +ktor-server-auth = { group = "io.ktor", name = "ktor-server-auth" } +ktor-server-sse = { group = "io.ktor", name = "ktor-server-sse" } +mcp-kotlin-client = { group = "io.modelcontextprotocol", name = "kotlin-sdk-client", version.ref = "mcp-kotlin" } +mcp-kotlin-server = { group = "io.modelcontextprotocol", name = "kotlin-sdk-server", version.ref = "mcp-kotlin" } +ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio" } +junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junit" } +slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } + +[plugins] +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } diff --git a/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.jar b/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..eddabd2 Binary files /dev/null and b/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.jar differ diff --git a/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.properties b/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ad7845b --- /dev/null +++ b/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/samples/simple-streamable-server/gradlew b/samples/simple-streamable-server/gradlew new file mode 100755 index 0000000..249efbb --- /dev/null +++ b/samples/simple-streamable-server/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/samples/simple-streamable-server/gradlew.bat b/samples/simple-streamable-server/gradlew.bat new file mode 100644 index 0000000..8508ef6 --- /dev/null +++ b/samples/simple-streamable-server/gradlew.bat @@ -0,0 +1,82 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/samples/simple-streamable-server/settings.gradle.kts b/samples/simple-streamable-server/settings.gradle.kts new file mode 100644 index 0000000..3c36808 --- /dev/null +++ b/samples/simple-streamable-server/settings.gradle.kts @@ -0,0 +1,25 @@ +rootProject.name = "simple-streamable-server" + +plugins { + // Apply the foojay-resolver plugin to allow automatic download of JDKs + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} + +dependencyResolutionManagement { + repositories { + mavenLocal() + mavenCentral() + } + + versionCatalogs { + create("libs") { + val mcpKotlinVersion = providers.gradleProperty( + "mcp.kotlin.overrideVersion", + ).orNull + if (mcpKotlinVersion != null) { + logger.lifecycle("Using the override version $mcpKotlinVersion of MCP Kotlin SDK") + version("mcp-kotlin", mcpKotlinVersion) + } + } + } +} diff --git a/samples/simple-streamable-server/src/main/kotlin/io/modelcontextprotocol/sample/server/main.kt b/samples/simple-streamable-server/src/main/kotlin/io/modelcontextprotocol/sample/server/main.kt new file mode 100644 index 0000000..161edc7 --- /dev/null +++ b/samples/simple-streamable-server/src/main/kotlin/io/modelcontextprotocol/sample/server/main.kt @@ -0,0 +1,28 @@ +package io.modelcontextprotocol.sample.server + +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty + +fun main(vararg args: String) { + val authEnabled = args.any { it == "--auth" } + val port = args.firstOrNull { it != "--auth" }?.toIntOrNull() ?: 3001 + val authToken = if (authEnabled) { + val envToken = System.getenv("MCP_AUTH_TOKEN") + requireNotNull(envToken) { + "MCP_AUTH_TOKEN environment variable must be set when using --auth" + } + envToken + } else { + null + } + + println("Starting MCP Streamable HTTP server on port $port") + println("Use MCP inspector to connect to http://localhost:$port/mcp") + if (authToken != null) { + println("Bearer auth enabled (token sourced from MCP_AUTH_TOKEN)") + } + + embeddedServer(Netty, host = "127.0.0.1", port = port) { + configureServer(authToken) + }.start(wait = true) +} diff --git a/samples/simple-streamable-server/src/main/kotlin/io/modelcontextprotocol/sample/server/server.kt b/samples/simple-streamable-server/src/main/kotlin/io/modelcontextprotocol/sample/server/server.kt new file mode 100644 index 0000000..cdffdae --- /dev/null +++ b/samples/simple-streamable-server/src/main/kotlin/io/modelcontextprotocol/sample/server/server.kt @@ -0,0 +1,297 @@ +package io.modelcontextprotocol.sample.server + +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpMethod +import io.ktor.http.HttpStatusCode +import io.ktor.serialization.kotlinx.json.json +import io.ktor.server.application.Application +import io.ktor.server.application.ApplicationCall +import io.ktor.server.application.install +import io.ktor.server.auth.Authentication +import io.ktor.server.auth.authenticate +import io.ktor.server.auth.bearer +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation +import io.ktor.server.plugins.cors.routing.CORS +import io.ktor.server.request.header +import io.ktor.server.response.respond +import io.ktor.server.routing.delete +import io.ktor.server.routing.post +import io.ktor.server.routing.route +import io.ktor.server.routing.routing +import io.ktor.server.sse.SSE +import io.ktor.server.sse.sse +import io.ktor.util.collections.ConcurrentMap +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import io.modelcontextprotocol.kotlin.sdk.server.StreamableHttpServerTransport +import io.modelcontextprotocol.kotlin.sdk.server.mcpStreamableHttp +import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel +import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification +import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotificationParams +import io.modelcontextprotocol.kotlin.sdk.types.McpJson +import io.modelcontextprotocol.kotlin.sdk.types.PromptArgument +import io.modelcontextprotocol.kotlin.sdk.types.PromptMessage +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult +import io.modelcontextprotocol.kotlin.sdk.types.Role +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents +import io.modelcontextprotocol.kotlin.sdk.types.ToolAnnotations +import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema +import kotlinx.coroutines.delay +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonObject +import kotlin.time.Duration.Companion.milliseconds + +private const val MCP_SESSION_ID_HEADER = "mcp-session-id" + +fun Application.configureServer(authToken: String? = null) { + installCors(authEnabled = authToken != null) + install(ContentNegotiation) { + json(McpJson) + } + + if (authToken == null) { + mcpStreamableHttp { + createMcpServer() + } + } else { + configureAuthenticatedMcp(authToken) + } +} + +private fun Application.configureAuthenticatedMcp(authToken: String) { + install(SSE) + install(Authentication) { + bearer("mcp-bearer") { + authenticate { credential -> + if (credential.token == authToken) { + io.ktor.server.auth.UserIdPrincipal("mcp-client") + } else { + null + } + } + } + } + + val transports = ConcurrentMap() + + routing { + authenticate("mcp-bearer") { + route("/mcp") { + sse { + val transport = findTransport(call, transports) ?: return@sse + transport.handleRequest(this, call) + } + + post { + val transport = getOrCreateTransport(call, transports) ?: return@post + transport.handleRequest(null, call) + } + + delete { + val transport = findTransport(call, transports) ?: return@delete + transport.handleRequest(null, call) + } + } + } + } +} + +private suspend fun findTransport( + call: ApplicationCall, + transports: ConcurrentMap, +): StreamableHttpServerTransport? { + val sessionId = call.request.header(MCP_SESSION_ID_HEADER) + if (sessionId.isNullOrEmpty()) { + call.respond(HttpStatusCode.BadRequest, "Bad Request: No valid session ID provided") + return null + } + val transport = transports[sessionId] + if (transport == null) { + call.respond(HttpStatusCode.NotFound, "Session not found") + return null + } + return transport +} + +private suspend fun getOrCreateTransport( + call: ApplicationCall, + transports: ConcurrentMap, +): StreamableHttpServerTransport? { + val sessionId = call.request.header(MCP_SESSION_ID_HEADER) + if (sessionId != null) { + val transport = transports[sessionId] + if (transport == null) { + call.respond(HttpStatusCode.NotFound, "Session not found") + } + return transport + } + + val configuration = StreamableHttpServerTransport.Configuration( + enableJsonResponse = true, + ) + val transport = StreamableHttpServerTransport(configuration) + + transport.setOnSessionInitialized { initializedSessionId -> + transports[initializedSessionId] = transport + } + transport.setOnSessionClosed { closedSessionId -> + transports.remove(closedSessionId) + } + + val server = createMcpServer() + server.onClose { + transport.sessionId?.let { transports.remove(it) } + } + server.createSession(transport) + + return transport +} + +private fun Application.installCors(authEnabled: Boolean = false) { + install(CORS) { + anyHost() // Don't do this in production if possible. Try to limit it. + allowMethod(HttpMethod.Options) + allowMethod(HttpMethod.Get) + allowMethod(HttpMethod.Post) + allowMethod(HttpMethod.Delete) + allowNonSimpleContentTypes = true + allowHeader("Mcp-Session-Id") + allowHeader("Mcp-Protocol-Version") + exposeHeader("Mcp-Session-Id") + exposeHeader("Mcp-Protocol-Version") + if (authEnabled) { + allowHeader(HttpHeaders.Authorization) + } + } +} + +private fun createMcpServer(): Server { + val server = Server( + Implementation( + name = "simple-streamable-http-server", + version = "1.0.0", + ), + ServerOptions( + capabilities = ServerCapabilities( + prompts = ServerCapabilities.Prompts(listChanged = true), + resources = ServerCapabilities.Resources(subscribe = true, listChanged = true), + tools = ServerCapabilities.Tools(listChanged = true), + logging = ServerCapabilities.Logging, + ), + ), + ) + + // Tool: greet + server.addTool( + name = "greet", + description = "A simple greeting tool", + inputSchema = ToolSchema( + properties = buildJsonObject { + putJsonObject("name") { + put("type", "string") + put("description", "Name to greet") + } + }, + required = listOf("name"), + ), + ) { request -> + val name = request.arguments?.get("name")?.jsonPrimitive?.content ?: "World" + CallToolResult(content = listOf(TextContent("Hello, $name!"))) + } + + // Tool: multi-greet (demonstrates logging notifications) + server.addTool( + name = "multi-greet", + description = "A tool that sends different greetings with delays between them", + inputSchema = ToolSchema( + properties = buildJsonObject { + putJsonObject("name") { + put("type", "string") + put("description", "Name to greet") + } + }, + required = listOf("name"), + ), + toolAnnotations = ToolAnnotations(readOnlyHint = true, openWorldHint = false), + ) { request -> + val name = request.arguments?.get("name")?.jsonPrimitive?.content ?: "World" + + sendLoggingMessage( + LoggingMessageNotification( + LoggingMessageNotificationParams( + level = LoggingLevel.Debug, + data = JsonPrimitive("Starting multi-greet for $name") + ) + ) + ) + delay(1000.milliseconds) + + sendLoggingMessage( + LoggingMessageNotification( + LoggingMessageNotificationParams( + level = LoggingLevel.Info, + data = JsonPrimitive("Sending first greeting to $name") + ) + ) + ) + delay(1000.milliseconds) + + sendLoggingMessage( + LoggingMessageNotification( + LoggingMessageNotificationParams( + level = LoggingLevel.Info, + data = JsonPrimitive("Sending second greeting to $name") + ) + ) + ) + + CallToolResult(content = listOf(TextContent("Good morning, $name!"))) + } + + // Prompt: greeting-template + server.addPrompt( + name = "greeting-template", + description = "A simple greeting prompt template", + arguments = listOf( + PromptArgument( + name = "name", + description = "Name to include in greeting", + required = true, + ), + ), + ) { request -> + val name = request.arguments?.get("name") ?: "World" + GetPromptResult( + messages = listOf( + PromptMessage( + role = Role.User, + content = TextContent("Please greet $name in a friendly manner."), + ), + ), + ) + } + + // Resource: greeting-resource + server.addResource( + uri = "https://example.com/greetings/default", + name = "Default Greeting", + description = "A simple greeting resource", + mimeType = "text/plain", + ) { request -> + ReadResourceResult( + contents = listOf( + TextResourceContents("Hello, world!", request.uri, "text/plain"), + ), + ) + } + + return server +} diff --git a/samples/simple-streamable-server/src/test/kotlin/io/modelcontextprotocol/sample/server/ServerIntegrationTest.kt b/samples/simple-streamable-server/src/test/kotlin/io/modelcontextprotocol/sample/server/ServerIntegrationTest.kt new file mode 100644 index 0000000..c4dae46 --- /dev/null +++ b/samples/simple-streamable-server/src/test/kotlin/io/modelcontextprotocol/sample/server/ServerIntegrationTest.kt @@ -0,0 +1,146 @@ +package io.modelcontextprotocol.sample.server + +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.sse.SSE +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.mcpStreamableHttp +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.Role +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ServerIntegrationTest { + + private val server = embeddedServer(Netty, host = "127.0.0.1", port = 0) { + configureServer() + } + + private val httpClient = HttpClient(CIO) { + install(SSE) + } + + private lateinit var mcpClient: Client + + @BeforeAll + suspend fun beforeAll() { + server.start(wait = false) + val port = server.engine.resolvedConnectors().single().port + + mcpClient = httpClient.mcpStreamableHttp( + url = "http://127.0.0.1:$port/mcp", + ) + } + + @AfterAll + suspend fun afterAll() { + mcpClient.close() + httpClient.close() + server.stop(500, 1000) + } + + @Test + fun `should negotiate server capabilities`() { + val capabilities = mcpClient.serverCapabilities + assertNotNull(capabilities) + assertNotNull(capabilities.tools) + assertNotNull(capabilities.prompts) + assertNotNull(capabilities.resources) + assertNotNull(capabilities.logging) + } + + @Test + fun `should report server version`() { + val version = mcpClient.serverVersion + assertNotNull(version) + assertEquals("simple-streamable-http-server", version.name) + assertEquals("1.0.0", version.version) + } + + @Test + suspend fun `should list tools`() { + val result = mcpClient.listTools() + val toolNames = result.tools.map { it.name }.sorted() + assertEquals(listOf("greet", "multi-greet"), toolNames) + } + + @Test + suspend fun `should call greet tool`() { + val result = mcpClient.callTool(name = "greet", arguments = mapOf("name" to "Alice")) + val content = result.content.single() + assertIs(content) + assertEquals("Hello, Alice!", content.text) + } + + @Test + suspend fun `should call greet tool with default name`() { + val result = mcpClient.callTool(name = "greet", arguments = emptyMap()) + val content = result.content.single() + assertIs(content) + assertEquals("Hello, World!", content.text) + } + + @Test + suspend fun `should call multi-greet tool with logging notifications`() { + val result = mcpClient.callTool(name = "multi-greet", arguments = mapOf("name" to "Bob")) + val content = result.content.single() + assertIs(content) + assertEquals("Good morning, Bob!", content.text) + } + + @Test + suspend fun `should list prompts`() { + val result = mcpClient.listPrompts() + assertEquals(listOf("greeting-template"), result.prompts.map { it.name }) + } + + @Test + suspend fun `should get greeting template prompt`() { + val result = mcpClient.getPrompt( + GetPromptRequest( + GetPromptRequestParams( + name = "greeting-template", + arguments = mapOf("name" to "Charlie"), + ), + ), + ) + val message = result.messages.single() + assertEquals(Role.User, message.role) + val content = message.content + assertIs(content) + assertEquals("Please greet Charlie in a friendly manner.", content.text) + } + + @Test + suspend fun `should list resources`() { + val result = mcpClient.listResources() + assertEquals(listOf("Default Greeting"), result.resources.map { it.name }) + } + + @Test + suspend fun `should read default greeting resource`() { + val result = mcpClient.readResource( + ReadResourceRequest( + ReadResourceRequestParams(uri = "https://example.com/greetings/default"), + ), + ) + val content = result.contents.single() + assertIs(content) + assertEquals("Hello, world!", content.text) + assertEquals("text/plain", content.mimeType) + } +} + diff --git a/samples/simple-streamable-server/src/test/resources/simplelogger.properties b/samples/simple-streamable-server/src/test/resources/simplelogger.properties new file mode 100644 index 0000000..ad2d3de --- /dev/null +++ b/samples/simple-streamable-server/src/test/resources/simplelogger.properties @@ -0,0 +1,6 @@ +org.slf4j.simpleLogger.defaultLogLevel=INFO +org.slf4j.simpleLogger.showThreadName=true +org.slf4j.simpleLogger.showDateTime=false + +# Log level for specific packages or classes +org.slf4j.simpleLogger.log.io.modelcontextprotocol.sample.server=DEBUG diff --git a/samples/weather-stdio-server/README.md b/samples/weather-stdio-server/README.md index d1edd7f..f377d55 100644 --- a/samples/weather-stdio-server/README.md +++ b/samples/weather-stdio-server/README.md @@ -1,140 +1,67 @@ -# Kotlin MCP Weather STDIO Server +# Weather STDIO Server -This project demonstrates how to build a Model Context Protocol (MCP) server in Kotlin that provides weather-related -tools by consuming the National Weather Service (weather.gov) API. The server uses STDIO as the transport layer and -leverages the Kotlin MCP SDK to expose weather forecast and alert tools. +A minimal MCP server that exposes weather forecast and alert tools using the National Weather +Service API over STDIO transport. -For more information about the MCP SDK and protocol, please refer to -the [MCP documentation](https://modelcontextprotocol.io/introduction). +## Overview + +This sample shows how to build a STDIO-based MCP server in Kotlin. It registers two tools that +query the [weather.gov](https://www.weather.gov/) API — one for weather forecasts by +latitude/longitude and one for active alerts by US state. Because it uses STDIO, the server is +launched as a subprocess by an MCP client or a desktop application like Claude Desktop. ## Prerequisites -- Java 17 or later -- Gradle (or the Gradle wrapper provided with the project) -- Basic understanding of MCP concepts -- Basic understanding of Kotlin and Kotlin ecosystems (sush as kotlinx-serialization, coroutines, ktor) +- JDK 17+ +- Internet access (the server calls the weather.gov API at runtime) -## MCP Weather Server +## Build & Run -The project provides: - -- A lightweight MCP server built with Kotlin. -- STDIO transport layer implementation for server-client communication. -- Two weather tools: - - **Weather Forecast Tool** — returns details such as temperature, wind information, and a detailed forecast for a - given latitude/longitude. - - **Weather Alerts Tool** — returns active weather alerts for a given US state. - -## Building and running - -Use the Gradle wrapper to build the application. In a terminal run: +Run the server (it communicates via stdin/stdout): ```shell -./gradlew clean build -x test +./gradlew run ``` -To run the server: +### MCP Inspector + +Build the fat JAR first, then connect with the +[MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector): ```shell -java -jar build/libs/.jar +./gradlew build +npx @modelcontextprotocol/inspector -- java -jar samples/weather-stdio-server/build/libs/weather-stdio-server-0.1.0-all.jar ``` -> [!NOTE] -> The server uses STDIO transport, so it is typically launched in an environment where the client connects via standard -> input/output. +### Claude Desktop integration -## Tool Implementation - -The project registers two MCP tools using the Kotlin MCP SDK. Below is an overview of the core tool implementations: - -### 1. Weather Forecast Tool - -This tool fetches the weather forecast for a specific latitude and longitude using the `weather.gov` API. - -Example tool registration in Kotlin: - -```kotlin -server.addTool( - name = "get_forecast", - description = """ - Get weather forecast for a specific latitude/longitude - """.trimIndent(), - inputSchema = Tool.Input( - properties = JsonObject( - mapOf( - "latitude" to JsonObject(mapOf("type" to JsonPrimitive("number"))), - "longitude" to JsonObject(mapOf("type" to JsonPrimitive("number"))), - ) - ), - required = listOf("latitude", "longitude") - ) -) { request -> - // Implementation tool -} -``` - -### 2. Weather Alerts Tool - -This tool retrieves active weather alerts for a US state. - -Example tool registration in Kotlin: - -```kotlin -server.addTool( - name = "get_alerts", - description = """ - Get weather alerts for a US state. Input is Two-letter US state code (e.g. CA, NY) - """.trimIndent(), - inputSchema = Tool.Input( - properties = JsonObject( - mapOf( - "state" to JsonObject( - mapOf( - "type" to JsonPrimitive("string"), - "description" to JsonPrimitive("Two-letter US state code (e.g. CA, NY)") - ) - ), - ) - ), - required = listOf("state") - ) -) { request -> - // Implementation tool -} -``` - -## Client Integration - -### Kotlin Client Example - -Since the server uses STDIO for transport, the client typically connects via standard input/output streams. A sample -client implementation can be found in the tests, demonstrating how to send tool requests and process responses. - -### Claude for Desktop - -To integrate with Claude Desktop, add the following configuration to your Claude Desktop settings: +Add the following to your Claude Desktop configuration: ```json { - "mcpServers": { - "weather": { - "command": "java", - "args": [ - "-jar", - "/absolute/path/to/.jar" - ] + "mcpServers": { + "weather": { + "command": "java", + "args": [ + "-jar", + "/absolute/path/to/samples/weather-stdio-server/build/libs/weather-stdio-server-0.1.0-all.jar" + ] + } } - } } ``` -> [!NOTE] -> Replace `/absolute/path/to/.jar` with the actual absolute path to your built jar file. +## MCP Capabilities + +### Tools + +| Name | Description | +|----------------|------------------------------------------------------------------------------------------| +| `get_alerts` | Returns active weather alerts for a two-letter US `state` code (e.g. `CA`, `NY`). | +| `get_forecast` | Returns weather forecast for a given `latitude` / `longitude` using the weather.gov API. | ## Additional Resources -- [MCP Specification](https://spec.modelcontextprotocol.io/) +- [MCP Specification](https://modelcontextprotocol.io/specification/latest) - [Kotlin MCP SDK](https://github.com/modelcontextprotocol/kotlin-sdk) -- [Ktor Client Documentation](https://ktor.io/docs/welcome.html) -- [Kotlinx Serialization](https://kotlinlang.org/docs/serialization.html) - +- [National Weather Service API](https://www.weather.gov/documentation/services-web-api) diff --git a/samples/weather-stdio-server/build.gradle.kts b/samples/weather-stdio-server/build.gradle.kts index f39057f..d6f5fc2 100644 --- a/samples/weather-stdio-server/build.gradle.kts +++ b/samples/weather-stdio-server/build.gradle.kts @@ -13,14 +13,16 @@ group = "org.example" version = "0.1.0" dependencies { + implementation(dependencies.platform(libs.ktor.bom)) + implementation(libs.mcp.kotlin.server) implementation(libs.ktor.client.content.negotiation) implementation(libs.ktor.serialization.kotlinx.json) - implementation(libs.mcp.kotlin.server) - implementation(libs.ktor.server.cio) implementation(libs.ktor.client.cio) implementation(libs.slf4j.simple) + runtimeOnly(libs.kotlin.logging) testImplementation(kotlin("test")) + testImplementation(libs.mcp.kotlin.client) testImplementation(libs.kotlinx.coroutines.test) } diff --git a/samples/weather-stdio-server/gradle.properties b/samples/weather-stdio-server/gradle.properties new file mode 100644 index 0000000..32cead6 --- /dev/null +++ b/samples/weather-stdio-server/gradle.properties @@ -0,0 +1,5 @@ +org.gradle.configuration-cache=true +org.gradle.parallel=true +org.gradle.caching=true + +#mcp.kotlin.overrideVersion=0.0.1-SNAPSHOT diff --git a/samples/weather-stdio-server/gradle/libs.versions.toml b/samples/weather-stdio-server/gradle/libs.versions.toml new file mode 100644 index 0000000..9d609a9 --- /dev/null +++ b/samples/weather-stdio-server/gradle/libs.versions.toml @@ -0,0 +1,24 @@ +[versions] +coroutines = "1.11.0" +kotlin = "2.4.20" +ktor = "3.5.2" +logging = "8.0.4" +mcp-kotlin = "0.15.0" +shadow = "9.6.1" +slf4j = "2.0.19" + +[libraries] +kotlin-logging = { group = "io.github.oshai", name = "kotlin-logging-jvm", version.ref = "logging" } +kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" } +ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" } +ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio" } +ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation" } +ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json-jvm" } +mcp-kotlin-client = { group = "io.modelcontextprotocol", name = "kotlin-sdk-client", version.ref = "mcp-kotlin" } +mcp-kotlin-server = { group = "io.modelcontextprotocol", name = "kotlin-sdk-server", version.ref = "mcp-kotlin" } +slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } + +[plugins] +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } diff --git a/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.jar b/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..eddabd2 Binary files /dev/null and b/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.jar differ diff --git a/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.properties b/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ad7845b --- /dev/null +++ b/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/samples/weather-stdio-server/gradlew b/samples/weather-stdio-server/gradlew new file mode 100755 index 0000000..249efbb --- /dev/null +++ b/samples/weather-stdio-server/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/samples/weather-stdio-server/gradlew.bat b/samples/weather-stdio-server/gradlew.bat new file mode 100644 index 0000000..8508ef6 --- /dev/null +++ b/samples/weather-stdio-server/gradlew.bat @@ -0,0 +1,82 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/samples/weather-stdio-server/settings.gradle.kts b/samples/weather-stdio-server/settings.gradle.kts index 67e786b..559a770 100644 --- a/samples/weather-stdio-server/settings.gradle.kts +++ b/samples/weather-stdio-server/settings.gradle.kts @@ -12,9 +12,16 @@ dependencyResolutionManagement { mavenLocal() mavenCentral() } + versionCatalogs { create("libs") { - from(files("../../gradle/libs.versions.toml")) + val mcpKotlinVersion = providers.gradleProperty( + "mcp.kotlin.overrideVersion", + ).orNull + if (mcpKotlinVersion != null) { + logger.lifecycle("Using the override version $mcpKotlinVersion of MCP Kotlin SDK") + version("mcp-kotlin", mcpKotlinVersion) + } } } } diff --git a/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/McpWeatherServer.kt b/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/McpWeatherServer.kt index 6bdb84d..1dee706 100644 --- a/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/McpWeatherServer.kt +++ b/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/McpWeatherServer.kt @@ -1,124 +1,186 @@ package io.modelcontextprotocol.sample.server -import io.ktor.client.* -import io.ktor.client.plugins.* -import io.ktor.client.plugins.contentnegotiation.* -import io.ktor.http.* -import io.ktor.serialization.kotlinx.json.* -import io.ktor.utils.io.streams.* -import io.modelcontextprotocol.kotlin.sdk.* +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.defaultRequest +import io.ktor.http.ContentType +import io.ktor.http.contentType +import io.ktor.http.headers +import io.ktor.serialization.kotlinx.json.json +import io.ktor.utils.io.streams.asInput import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport +import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.ToolAnnotations +import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema import kotlinx.coroutines.Job import kotlinx.coroutines.runBlocking import kotlinx.io.asSink import kotlinx.io.buffered -import kotlinx.serialization.json.* +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.doubleOrNull +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonObject -// Main function to run the MCP server -fun `run mcp server`() { - // Base URL for the Weather API - val baseUrl = "https://api.weather.gov" +/** + * Starts an MCP server that provides weather-related tools for fetching active + * weather alerts by state and weather forecasts by latitude/longitude. + */ +fun runMcpServer() { + createHttpClient().use { httpClient -> + val server = Server( + Implementation( + name = "weather", + version = "1.0.0", + ), + ServerOptions( + capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true)), + ), + ) - // Create an HTTP client with a default request configuration and JSON content negotiation - val httpClient = HttpClient { - defaultRequest { - url(baseUrl) - headers { - append("Accept", "application/geo+json") - append("User-Agent", "WeatherApiClient/1.0") + server.registerTools(httpClient) + + val transport = StdioServerTransport( + System.`in`.asInput(), + System.out.asSink().buffered(), + ) + + runBlocking { + val session = server.createSession(transport) + val done = Job() + session.onClose { + done.complete() } - contentType(ContentType.Application.Json) - } - // Install content negotiation plugin for JSON serialization/deserialization - install(ContentNegotiation) { - json(Json { - ignoreUnknownKeys = true - prettyPrint = true - }) + done.join() } } +} - // Create the MCP Server instance with a basic implementation - val server = Server( - Implementation( - name = "weather", // Tool name is "weather" - version = "1.0.0" // Version of the implementation - ), - ServerOptions( - capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true)) +private fun createHttpClient(): HttpClient = HttpClient(CIO) { + defaultRequest { + url("https://api.weather.gov") + headers { + append("Accept", "application/geo+json") + append("User-Agent", "WeatherApiClient/1.0") + } + contentType(ContentType.Application.Json) + } + install(ContentNegotiation) { + json( + Json { + ignoreUnknownKeys = true + prettyPrint = true + }, ) - ) + } +} +private fun Server.registerTools(httpClient: HttpClient) { // Register a tool to fetch weather alerts by state - server.addTool( + addTool( name = "get_alerts", - description = """ - Get weather alerts for a US state. Input is Two-letter US state code (e.g. CA, NY) - """.trimIndent(), - inputSchema = Tool.Input( + description = "Get weather alerts for a US state. Input is a two-letter US state code (e.g. CA, NY)", + inputSchema = ToolSchema( properties = buildJsonObject { putJsonObject("state") { put("type", "string") put("description", "Two-letter US state code (e.g. CA, NY)") } }, - required = listOf("state") - ) + required = listOf("state"), + ), + toolAnnotations = ToolAnnotations(readOnlyHint = true, openWorldHint = true), ) { request -> - val state = request.arguments["state"]?.jsonPrimitive?.content ?: return@addTool CallToolResult( - content = listOf(TextContent("The 'state' parameter is required.")) - ) + val state = request.arguments?.get("state")?.jsonPrimitive?.content + ?: return@addTool CallToolResult( + content = listOf(TextContent("The 'state' parameter is required.")), + ) - val alerts = httpClient.getAlerts(state) - - CallToolResult(content = alerts.map { TextContent(it) }) - } - - // Register a tool to fetch weather forecast by latitude and longitude - server.addTool( - name = "get_forecast", - description = """ - Get weather forecast for a specific latitude/longitude - """.trimIndent(), - inputSchema = Tool.Input( - properties = buildJsonObject { - putJsonObject("latitude") { - put("type", "number") - } - putJsonObject("longitude") { - put("type", "number") - } - }, - required = listOf("latitude", "longitude") - ) - ) { request -> - val latitude = request.arguments["latitude"]?.jsonPrimitive?.doubleOrNull - val longitude = request.arguments["longitude"]?.jsonPrimitive?.doubleOrNull - if (latitude == null || longitude == null) { + if (state.length != 2) { return@addTool CallToolResult( - content = listOf(TextContent("The 'latitude' and 'longitude' parameters are required.")) + content = listOf(TextContent("Invalid state code: '$state'. Must be a two-letter US state code.")), + isError = true, ) } - val forecast = httpClient.getForecast(latitude, longitude) + val stateCode = state.uppercase() - CallToolResult(content = forecast.map { TextContent(it) }) + httpClient.getAlerts(stateCode).fold( + onSuccess = { alerts -> + if (alerts.isEmpty()) { + CallToolResult(content = listOf(TextContent("No active alerts for $stateCode"))) + } else { + val alertsText = "Active alerts for $stateCode:\n\n${alerts.joinToString("\n---\n")}" + CallToolResult(content = listOf(TextContent(alertsText))) + } + }, + onFailure = { e -> + CallToolResult( + content = listOf(TextContent("Failed to retrieve alerts data: ${e.message}")), + isError = true, + ) + }, + ) } - // Create a transport using standard IO for server communication - val transport = StdioServerTransport( - System.`in`.asInput(), - System.out.asSink().buffered() - ) - - runBlocking { - server.connect(transport) - val done = Job() - server.onClose { - done.complete() + // Register a tool to fetch weather forecast by latitude and longitude + addTool( + name = "get_forecast", + description = "Get weather forecast for a location. Note: only US locations are supported by the NWS API.", + inputSchema = ToolSchema( + properties = buildJsonObject { + putJsonObject("latitude") { + put("type", "number") + put("description", "Latitude of the location") + put("minimum", -90) + put("maximum", 90) + } + putJsonObject("longitude") { + put("type", "number") + put("description", "Longitude of the location") + put("minimum", -180) + put("maximum", 180) + } + }, + required = listOf("latitude", "longitude"), + ), + toolAnnotations = ToolAnnotations(readOnlyHint = true, openWorldHint = true), + ) { request -> + val latitude = request.arguments?.get("latitude")?.jsonPrimitive?.doubleOrNull + val longitude = request.arguments?.get("longitude")?.jsonPrimitive?.doubleOrNull + if (latitude == null || longitude == null) { + return@addTool CallToolResult( + content = listOf(TextContent("The 'latitude' and 'longitude' parameters are required.")), + ) } - done.join() + + httpClient.getForecast(latitude, longitude).fold( + onSuccess = { periods -> + if (periods.isEmpty()) { + CallToolResult(content = listOf(TextContent("No forecast periods available"))) + } else { + val forecastText = periods.joinToString("\n---\n") + CallToolResult(content = listOf(TextContent(forecastText))) + } + }, + onFailure = { _ -> + CallToolResult( + content = listOf( + TextContent( + "Failed to retrieve grid point data for coordinates: $latitude, $longitude. " + + "This location may not be supported by the NWS API (only US locations are supported).", + ), + ), + isError = true, + ) + }, + ) } } diff --git a/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/WeatherApi.kt b/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/WeatherApi.kt index 671186c..000ac12 100644 --- a/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/WeatherApi.kt +++ b/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/WeatherApi.kt @@ -4,88 +4,90 @@ import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.request.get import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonObject - -// Extension function to fetch forecast information for given latitude and longitude -suspend fun HttpClient.getForecast(latitude: Double, longitude: Double): List { - // Build the URI using provided latitude and longitude - val uri = "/points/$latitude,$longitude" - // Request the points data from the API - val points = this.get(uri).body() - - // Request the forecast using the URL provided in the points response - val forecast = this.get(points.properties.forecast).body() - - // Map each forecast period to a formatted string - return forecast.properties.periods.map { period -> - """ - ${period.name}: - Temperature: ${period.temperature} ${period.temperatureUnit} - Wind: ${period.windSpeed} ${period.windDirection} - Forecast: ${period.detailedForecast} - """.trimIndent() - } -} +import java.util.Locale // Extension function to fetch weather alerts for a given state -suspend fun HttpClient.getAlerts(state: String): List { - // Build the URI using the given state code +suspend fun HttpClient.getAlerts(state: String): Result> = runCatching { val uri = "/alerts/active/area/$state" - // Request the alerts data from the API - val alerts = this.get(uri).body() + val alerts = this.get(uri).body() - // Map each alert feature to a formatted string - return alerts.features.map { feature -> + alerts.features.map { feature -> """ Event: ${feature.properties.event} Area: ${feature.properties.areaDesc} Severity: ${feature.properties.severity} - Description: ${feature.properties.description} - Instruction: ${feature.properties.instruction} + Status: ${feature.properties.status} + Headline: ${feature.properties.headline} """.trimIndent() } } -// Data class representing the points response from the API -@Serializable -data class Points( - val properties: Properties -) { - @Serializable - data class Properties(val forecast: String) +// Extension function to fetch forecast information for given latitude and longitude +suspend fun HttpClient.getForecast(latitude: Double, longitude: Double): Result> = runCatching { + val lat = String.format(Locale.US, "%.4f", latitude) + val lon = String.format(Locale.US, "%.4f", longitude) + val uri = "/points/$lat,$lon" + val points = this.get(uri).body() + + val forecastUrl = points.properties.forecast + ?: error("No forecast URL available for coordinates: $latitude, $longitude") + val forecast = this.get(forecastUrl).body() + + forecast.properties.periods.map { period -> + """ + ${period.name}: + Temperature: ${period.temperature}°${period.temperatureUnit} + Wind: ${period.windSpeed} ${period.windDirection} + ${period.shortForecast} + """.trimIndent() + } } -// Data class representing the forecast response from the API @Serializable -data class Forecast( - val properties: Properties -) { - @Serializable - data class Properties(val periods: List) +data class PointsResponse( + val properties: PointsProperties, +) - @Serializable - data class Period( - val number: Int, val name: String, val startTime: String, val endTime: String, - val isDaytime: Boolean, val temperature: Int, val temperatureUnit: String, - val temperatureTrend: String, val probabilityOfPrecipitation: JsonObject, - val windSpeed: String, val windDirection: String, - val shortForecast: String, val detailedForecast: String, - ) -} - -// Data class representing the alerts response from the API @Serializable -data class Alert( - val features: List -) { - @Serializable - data class Feature( - val properties: Properties - ) +data class PointsProperties( + val forecast: String? = null, +) - @Serializable - data class Properties( - val event: String, val areaDesc: String, val severity: String, - val description: String, val instruction: String?, - ) -} +@Serializable +data class ForecastResponse( + val properties: ForecastProperties, +) + +@Serializable +data class ForecastProperties( + val periods: List = emptyList(), +) + +@Serializable +data class ForecastPeriod( + val name: String? = null, + val temperature: Int? = null, + val temperatureUnit: String? = null, + val windSpeed: String? = null, + val windDirection: String? = null, + val shortForecast: String? = null, +) + +@Serializable +data class AlertsResponse( + val features: List = emptyList(), +) + +@Serializable +data class AlertFeature( + val properties: AlertProperties, +) + +@Serializable +data class AlertProperties( + val event: String? = null, + val areaDesc: String? = null, + val severity: String? = null, + val status: String? = null, + val headline: String? = null, +) diff --git a/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/main.kt b/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/main.kt index 21100dd..3d225cd 100644 --- a/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/main.kt +++ b/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/main.kt @@ -1,3 +1,3 @@ package io.modelcontextprotocol.sample.server -fun main() = `run mcp server`() \ No newline at end of file +fun main() = runMcpServer() diff --git a/samples/weather-stdio-server/src/test/kotlin/io/modelcontextprotocol/sample/client/ClientStdio.kt b/samples/weather-stdio-server/src/test/kotlin/io/modelcontextprotocol/sample/client/ClientStdio.kt index 20c2a79..32fd550 100644 --- a/samples/weather-stdio-server/src/test/kotlin/io/modelcontextprotocol/sample/client/ClientStdio.kt +++ b/samples/weather-stdio-server/src/test/kotlin/io/modelcontextprotocol/sample/client/ClientStdio.kt @@ -1,25 +1,25 @@ package io.modelcontextprotocol.sample.client -import io.modelcontextprotocol.kotlin.sdk.CallToolRequest -import io.modelcontextprotocol.kotlin.sdk.Implementation -import io.modelcontextprotocol.kotlin.sdk.TextContent import io.modelcontextprotocol.kotlin.sdk.client.Client import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.TextContent import kotlinx.coroutines.runBlocking import kotlinx.io.asSink import kotlinx.io.asSource import kotlinx.io.buffered -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive - fun main(): Unit = runBlocking { - val process = ProcessBuilder("java", "-jar", "build/libs/weather-stdio-server-0.1.0-all.jar") + val process = ProcessBuilder( + "java", + "-jar", + "${System.getProperty("user.dir")}/samples/weather-stdio-server/build/libs/weather-stdio-server-0.1.0-all.jar", + ).redirectError(ProcessBuilder.Redirect.INHERIT) .start() val transport = StdioClientTransport( input = process.inputStream.asSource().buffered(), - output = process.outputStream.asSink().buffered() + output = process.outputStream.asSink().buffered(), ) // Initialize the MCP client with client information @@ -29,28 +29,26 @@ fun main(): Unit = runBlocking { client.connect(transport) - - val toolsList = client.listTools()?.tools?.map { it.name } + val toolsList = client.listTools().tools.map { it.name } println("Available Tools = $toolsList") val weatherForecastResult = client.callTool( - CallToolRequest( - name = "get_forecast", - arguments = JsonObject(mapOf("latitude" to JsonPrimitive(38.5816), "longitude" to JsonPrimitive(-121.4944))) - ) - )?.content?.map { if (it is TextContent) it.text else it.toString() } + name = "get_forecast", + arguments = mapOf( + "latitude" to 38.5816, + "longitude" to -121.4944, + ), + ).content.map { if (it is TextContent) it.text else it.toString() } - println("Weather Forcast: ${weatherForecastResult?.joinToString(separator = "\n", prefix = "\n", postfix = "\n")}") + println("Weather Forecast: ${weatherForecastResult.joinToString(separator = "\n", prefix = "\n", postfix = "\n")}") val alertResult = client.callTool( - CallToolRequest( - name = "get_alerts", - arguments = JsonObject(mapOf("state" to JsonPrimitive("TX"))) - ) - )?.content?.map { if (it is TextContent) it.text else it.toString() } + name = "get_alerts", + arguments = mapOf("state" to "TX"), + ).content.map { if (it is TextContent) it.text else it.toString() } println("Alert Response = $alertResult") client.close() -} \ No newline at end of file +} diff --git a/samples/weather-stdio-server/src/test/kotlin/io/modelcontextprotocol/sample/client/FakeTest.kt b/samples/weather-stdio-server/src/test/kotlin/io/modelcontextprotocol/sample/client/FakeTest.kt new file mode 100644 index 0000000..40b4013 --- /dev/null +++ b/samples/weather-stdio-server/src/test/kotlin/io/modelcontextprotocol/sample/client/FakeTest.kt @@ -0,0 +1,11 @@ +package io.modelcontextprotocol.sample.client + +import kotlin.test.Test + +class FakeTest { + + @Test + fun testSomething() { + assert(true) { "Bingo!" } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 048676f..60e3d1c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,14 +18,13 @@ dependencyResolutionManagement { } include( + ":test-utils", ":kotlin-sdk-core", ":kotlin-sdk-client", ":kotlin-sdk-server", + ":kotlin-sdk-testing", ":kotlin-sdk", - ":kotlin-sdk-test", + ":integration-test", + ":docs", + ":conformance-test", ) - -// Include sample projects as composite builds -includeBuild("samples/kotlin-mcp-client") -includeBuild("samples/kotlin-mcp-server") -includeBuild("samples/weather-stdio-server") diff --git a/test-utils/build.gradle.kts b/test-utils/build.gradle.kts new file mode 100644 index 0000000..c6baf2d --- /dev/null +++ b/test-utils/build.gradle.kts @@ -0,0 +1,55 @@ +@file:OptIn(ExperimentalWasmDsl::class) + +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl + +plugins { + id("mcp.multiplatform") + id("mcp.detekt") +} + +kotlin { + + iosArm64() + iosX64() + iosSimulatorArm64() + watchosArm64() + watchosSimulatorArm64() + tvosArm64() + tvosSimulatorArm64() + js { + browser() + nodejs() + } + wasmJs { + browser() + nodejs() + } + + sourceSets { + commonMain { + dependencies { + + api(kotlin("test")) + api(libs.kotest.assertions.core) + api(libs.kotest.assertions.json) + api(libs.kotlinx.serialization.json) + api(libs.kotlinx.coroutines.test) + api(libs.ktor.server.core) + api(libs.kotlin.logging) + } + } + + jvmMain { + dependencies { + api(libs.mockk) + api(libs.awaitility) + api(libs.junit.jupiter.params) + runtimeOnly(libs.slf4j.simple) + } + } + } +} + +tasks.withType { + useJUnitPlatform() +} diff --git a/test-utils/detekt-baseline-main.xml b/test-utils/detekt-baseline-main.xml new file mode 100644 index 0000000..431f3d9 --- /dev/null +++ b/test-utils/detekt-baseline-main.xml @@ -0,0 +1,20 @@ + + + + + ForbiddenMethodCall:Retry.kt:RetryExtension$println( "[Retry] Failure on attempt $attempt/$maxAttempts for ${ describeTest( extensionContext, ) }: ${lastError.message}", ) + ForbiddenMethodCall:Retry.kt:RetryExtension$println( "[Retry] Giving up after $attempt attempts for ${ describeTest( extensionContext, ) }: ${lastError.message}", ) + ForbiddenMethodCall:TypeScriptRunner.kt:TypeScriptRunner$println("Adding exports to ${file.absolutePath}") + ForbiddenMethodCall:TypeScriptRunner.kt:TypeScriptRunner$println("Command failed: ${e.message}") + ForbiddenMethodCall:TypeScriptRunner.kt:TypeScriptRunner$println("Detected pnpm-specific protocols or workspace. Attempting pnpm install...") + ForbiddenMethodCall:TypeScriptRunner.kt:TypeScriptRunner$println("Executing: ${fullCommand.joinToString(" ")}") + ForbiddenMethodCall:TypeScriptRunner.kt:TypeScriptRunner$println("Installing TypeScript dependencies in ${typescriptDir.absolutePath}") + ForbiddenMethodCall:TypeScriptRunner.kt:TypeScriptRunner$println("[TS-INSTALL] $it") + ForbiddenMethodCall:TypeScriptRunner.kt:TypeScriptRunner$println("npm install failed. Patching package.json and trying again...") + ForbiddenMethodCall:processUtils.kt:println("Warning: Error reading process error stream: ${e.message}") + ForbiddenMethodCall:processUtils.kt:println("Warning: Error reading process output: ${e.message}") + ForbiddenMethodCall:processUtils.kt:println("[$prefix] $it") + ForbiddenMethodCall:processUtils.kt:println("[$prefix][err] $it") + ForbiddenVoid:Retry.kt:RetryExtension$Void + + diff --git a/test-utils/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/ktor.kt b/test-utils/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/ktor.kt new file mode 100644 index 0000000..cf3e4d1 --- /dev/null +++ b/test-utils/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/ktor.kt @@ -0,0 +1,17 @@ +package io.modelcontextprotocol.kotlin.test.utils + +import io.ktor.server.engine.EmbeddedServer + +/** + * Retrieves the actual port that the embedded server is bound to. + * + * This function resolves the server's connectors and retrieves the port + * from the single resolved connector. It is useful for obtaining the + * port dynamically in cases where the server is bound to a random port. + * + * Note: This method assumes the server has exactly one resolved connector. + * + * @receiver The embedded server instance + * @return The port number the server is bound to + */ +public suspend fun EmbeddedServer<*, *>.actualPort(): Int = engine.resolvedConnectors().single().port diff --git a/test-utils/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/serializationUtils.kt b/test-utils/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/serializationUtils.kt new file mode 100644 index 0000000..8f3d33a --- /dev/null +++ b/test-utils/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/serializationUtils.kt @@ -0,0 +1,88 @@ +package io.modelcontextprotocol.kotlin.test.utils + +import io.kotest.assertions.json.shouldEqualJson +import io.kotest.assertions.withClue +import io.kotest.matchers.shouldBe +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import org.intellij.lang.annotations.Language +import kotlin.test.fail + +/** + * Verifies the serialization and deserialization process for a given object, ensuring: + * - The object can be serialized to JSON. + * - The serialized JSON matches the expected JSON. + * - The serialized JSON can be deserialized back into the original object. + * + * This is especially useful for round-trip serialization tests. + * + * @param T The type of the object to be verified. + * @param value The object to be serialized and deserialized. + * @param json The JSON serializer/deserializer used for the serialization process. + * @param expectedJson A lambda returning the expected JSON string for comparison purposes. + */ +public inline fun verifySerialization(value: T, json: Json, expectedJson: () -> String) { + verifySerialization(value, json, expectedJson.invoke()) +} + +/** + * Verifies the serialization and deserialization process for a given object, ensuring: + * - The object can be serialized to JSON. + * - The serialized JSON matches the expected JSON. + * - The serialized JSON can be deserialized back into the original object. + * + * This is especially useful for round-trip serialization tests. + * + * @param T The type of the object being tested for serialization and deserialization. + * @param value The object to be serialized and deserialized. + * @param json The JSON serialization/deserialization processor. + * @param expectedJson The expected JSON string to compare against the serialized output. + */ +public inline fun verifySerialization(value: T, json: Json, @Language("json") expectedJson: String) { + val jsonString = json.encodeToString(value) + + jsonString shouldEqualJson expectedJson + + val parsedObject: T = try { + json.decodeFromString(jsonString) + } catch (e: SerializationException) { + fail("Failed to parse generated json string: ${e.message}:\n$value", e) + } + withClue("Parsed object is not equal to original value") { + parsedObject shouldBe value + } +} + +/** + * Verifies the deserialization of a JSON payload into an object of the specified type, + * and ensures that the reverse serialization process returns the same JSON payload. + * + * This is especially useful for round-trip serialization tests. + * + * @param T The type of the object being deserialized from the JSON payload. + * @param json The JSON processor used for deserialization and serialization. + * @param payload The JSON string to deserialize into an object and verify against. + * @return The deserialized object of type T. + */ +public inline fun verifyDeserialization(json: Json, @Language("json") payload: String): T { + val value: T = json.decodeFromString(payload) + verifySerialization(value = value, json = json, expectedJson = payload) + return value +} + +/** + * Verifies the round-trip serialization and deserialization process for a given object. + * This method ensures that: + * - The object can be serialized into a JSON string. + * - The serialized JSON string can be deserialized back into an equivalent object. + * + * This is particularly useful for testing the integrity of serialization logic. + * + * @param T The type of the object being tested for serialization and deserialization. + * @param value The object to be serialized and deserialized. + * @param json The JSON processor used for the serialization and deserialization process. + */ +public inline fun verifySerializationRoundTrip(value: T, json: Json) { + val jsonString: String = json.encodeToString(value) + verifySerialization(value = value, json = json, expectedJson = jsonString) +} diff --git a/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/DisabledOnCI.kt b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/DisabledOnCI.kt new file mode 100644 index 0000000..5ff456b --- /dev/null +++ b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/DisabledOnCI.kt @@ -0,0 +1,6 @@ +package io.modelcontextprotocol.kotlin.test.utils + +import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable + +@DisabledIfEnvironmentVariable(named = "CI", matches = "true") +public annotation class DisabledOnCI diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/Retry.kt b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/Retry.kt similarity index 89% rename from kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/Retry.kt rename to test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/Retry.kt index 32f2053..1dcf585 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/Retry.kt +++ b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/Retry.kt @@ -1,4 +1,4 @@ -package io.modelcontextprotocol.kotlin.sdk.integration.utils +package io.modelcontextprotocol.kotlin.test.utils import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.ExtensionContext @@ -7,15 +7,16 @@ import org.junit.jupiter.api.extension.InvocationInterceptor.Invocation import org.junit.jupiter.api.extension.ReflectiveInvocationContext import org.opentest4j.TestAbortedException import java.lang.reflect.AnnotatedElement +import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method import java.util.Optional @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) @ExtendWith(RetryExtension::class) -annotation class Retry(val times: Int = 3, val delayMs: Long = 1000) +public annotation class Retry(val times: Int = 3, val delayMs: Long = 1000) -class RetryExtension : InvocationInterceptor { +public class RetryExtension : InvocationInterceptor { override fun interceptTestMethod( invocation: Invocation, invocationContext: ReflectiveInvocationContext, @@ -65,7 +66,7 @@ class RetryExtension : InvocationInterceptor { return } catch (t: Throwable) { if (t is TestAbortedException) throw t - lastError = if (t is java.lang.reflect.InvocationTargetException) t.targetException ?: t else t + lastError = if (t is InvocationTargetException) t.targetException ?: t else t if (attempt == maxAttempts) { println( "[Retry] Giving up after $attempt attempts for ${ @@ -86,7 +87,7 @@ class RetryExtension : InvocationInterceptor { } } - throw lastError ?: IllegalStateException("Unexpected state in retry logic") + throw lastError ?: error("Unexpected state in retry logic") } private fun describeTest(ctx: ExtensionContext): String { diff --git a/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/TypeScriptRunner.kt b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/TypeScriptRunner.kt new file mode 100644 index 0000000..ff9292e --- /dev/null +++ b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/TypeScriptRunner.kt @@ -0,0 +1,144 @@ +package io.modelcontextprotocol.kotlin.test.utils + +import org.junit.jupiter.api.fail +import java.io.File + +/** + * Component for running arbitrary TypeScript files using npx tsx. + */ +public object TypeScriptRunner { + + /** + * Runs a TypeScript file using npx tsx. + * + * @param typescriptDir The working directory for the process + * @param scriptPath The path to the TypeScript file relative to [typescriptDir] or absolute + * @param env Environment variables for the process + * @param redirectErrorStream Whether to redirect error stream to output stream + * @param logPrefix Prefix for log lines + * @return The started Process + */ + public fun run( + typescriptDir: File, + scriptPath: String, + arguments: List = emptyList(), + env: Map = emptyMap(), + redirectErrorStream: Boolean = false, + logPrefix: String = "TS-RUNNER", + log: Boolean = true, + ): Process { + val command = mutableListOf("npx", "tsx", scriptPath) + command.addAll(arguments) + val pb = ProcessBuilder(command) + pb.directory(typescriptDir) + env.forEach { (k, v) -> pb.environment()[k] = v } + pb.redirectErrorStream(redirectErrorStream) + + val proc = pb.start() + + if (log) { + if (redirectErrorStream) { + createProcessOutputReader(proc, logPrefix).start() + } else { + createProcessErrorReader(proc, logPrefix).start() + } + } + + return proc + } + + /** + * Installs npm dependencies in the specified TypeScript directory. + * + * @param typescriptDir The directory containing package.json + * @throws RuntimeException if npm install fails + */ + public fun installDependencies(typescriptDir: File) { + require(typescriptDir.isDirectory()) { "Type script directory does not exist: ${typescriptDir.absolutePath}" } + println("Installing TypeScript dependencies in ${typescriptDir.absolutePath}") + + val packageJson = File(typescriptDir, "package.json") + val content = if (packageJson.exists()) packageJson.readText() else "" + val hasCatalog = content.contains("catalog:") + val hasWorkspace = content.contains("workspace:") + val hasPnpmWorkspace = File(typescriptDir, "pnpm-workspace.yaml").exists() + + if (hasCatalog || hasWorkspace || hasPnpmWorkspace) { + println("Detected pnpm-specific protocols or workspace. Attempting pnpm install...") + if (tryCommand(listOf("pnpm", "install"), typescriptDir) || + tryCommand(listOf("npx", "pnpm", "install"), typescriptDir) + ) { + patchPackageJsonRecursively(typescriptDir, patchProtocols = false) + return + } + } + + // Try standard npm install + if (tryCommand(listOf("npm", "install"), typescriptDir)) { + patchPackageJsonRecursively(typescriptDir, patchProtocols = false) + return + } + + // If npm fails and we have catalog/workspace, try patching and npm again + println("npm install failed. Patching package.json and trying again...") + patchPackageJsonRecursively(typescriptDir, patchProtocols = true) + if (tryCommand(listOf("npm", "install"), typescriptDir)) return + + fail("Failed to install TypeScript dependencies in ${typescriptDir.absolutePath}") + } + + private fun tryCommand(command: List, workingDir: File): Boolean { + val fullCommand = if (isWindows) { + listOf("cmd.exe", "/c") + command + } else { + command + } + println("Executing: ${fullCommand.joinToString(" ")}") + return try { + val pb = ProcessBuilder(fullCommand) + pb.directory(workingDir) + pb.redirectErrorStream(true) + val proc = pb.start() + proc.inputStream.bufferedReader().useLines { lines -> + lines.forEach { println("[TS-INSTALL] $it") } + } + proc.waitFor() == 0 + } catch (e: Exception) { + println("Command failed: ${e.message}") + false + } + } + + private fun patchPackageJsonRecursively(dir: File, patchProtocols: Boolean) { + dir.walkTopDown() + .onEnter { it.name != "node_modules" } + .filter { it.name == "package.json" } + .forEach { file -> + var content = file.readText() + var modified = false + + if (patchProtocols) { + val newContent = content + .replace(Regex(""""catalog:[^"]*""""), "\"*\"") + .replace(Regex(""""workspace:[^"]*""""), "\"*\"") + .replace(Regex(""""link:[^"]*""""), "\"*\"") + if (newContent != content) { + content = newContent + modified = true + } + } + + // Add entry point if missing and src/index.ts exists to help tsx resolution + val srcIndex = File(file.parentFile, "src/index.ts") + if (srcIndex.exists() && !content.contains("\"exports\"") && !content.contains("\"main\"")) { + println("Adding exports to ${file.absolutePath}") + content = content.replace(Regex("""^\{"""), "{\n \"exports\": { \".\": \"./src/index.ts\" },") + modified = true + } + + if (modified) { + file.writeText(content) + } + } + } +} diff --git a/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/platform.jvm.kt b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/platform.jvm.kt new file mode 100644 index 0000000..ca34b6d --- /dev/null +++ b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/platform.jvm.kt @@ -0,0 +1,5 @@ +package io.modelcontextprotocol.kotlin.test.utils + +public val isWindows: Boolean = System.getProperty("os.name").lowercase().contains("windows") + +public val NPX: String = if (isWindows) "npx.cmd" else "npx" diff --git a/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/processBuilders.kt b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/processBuilders.kt new file mode 100644 index 0000000..519dfcd --- /dev/null +++ b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/processBuilders.kt @@ -0,0 +1,45 @@ +package io.modelcontextprotocol.kotlin.test.utils + +import io.github.oshai.kotlinlogging.KLogger +import java.io.BufferedReader +import java.io.InputStream +import java.io.InputStreamReader + +public fun createTeeProcessBuilder(): ProcessBuilder = if (isWindows) { + ProcessBuilder("cmd", "/c", "findstr", "^") +} else { + ProcessBuilder("tee") +} + +public fun createSleepyProcessBuilder(): ProcessBuilder = if (isWindows) { + ProcessBuilder("cmd", "/c", "ping -n 2 127.0.0.1 > nul && echo simulated error 1>&2 && exit 1") +} else { + ProcessBuilder("sh", "-c", "sleep 1 && echo 'simulated error' >&2 && exit 1") +} + +/** + * Starts logging the lines from this InputStream to a specified logger. + * Each line from the InputStream is read and logged using the provided logger + * at the debug level, optionally with an additional prefix string appended + * to each logged line. The method runs the logging process in a separate daemon thread. + * + * @param logger The logger used to log the lines from the InputStream. + * @param name The name to assign to the daemon thread handling the logging process. + */ +public fun InputStream.startLogging(logger: KLogger, name: String) { + val stream = this + Thread { + try { + BufferedReader(InputStreamReader(stream)).use { reader -> + reader.lineSequence().forEach { line -> + logger.debug { line } + } + } + } catch (e: Exception) { + logger.trace(e) { "Error reading server test stdout" } + } + }.apply { + this.name = name + isDaemon = true + }.start() +} diff --git a/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/processUtils.kt b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/processUtils.kt new file mode 100644 index 0000000..a80c825 --- /dev/null +++ b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/processUtils.kt @@ -0,0 +1,115 @@ +package io.modelcontextprotocol.kotlin.test.utils + +import org.awaitility.kotlin.await +import java.net.ServerSocket +import java.net.Socket +import java.util.concurrent.TimeUnit +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +/** + * Kills any process currently using the specified port. + * + * @param port The port number to free up + */ +public fun killProcessOnPort(port: Int) { + val killCommand = if (isWindows) { + listOf( + "cmd.exe", + "/c", + "netstat -ano | findstr :$port | for /f \"tokens=5\" %a in ('more') do taskkill /F /PID %a 2>nul || echo No process found", + ) + } else { + listOf("bash", "-c", "lsof -ti:$port | xargs kill -9 2>/dev/null || true") + } + ProcessBuilder(killCommand).start().waitFor() +} + +/** + * Waits for a port to become available (indicating server is ready). + * + * @param host The hostname to check (default: "localhost") + * @param port The port number to check + * @param timeoutSeconds Maximum time to wait in seconds (default: 10) + * @return true if port became available, false if timeout occurred + */ +public fun waitForPort(host: String = "localhost", port: Int, timeoutSeconds: Long = 10): Boolean = try { + await.atMost(timeoutSeconds, TimeUnit.SECONDS) + .pollDelay(200, TimeUnit.MILLISECONDS) + .pollInterval(100, TimeUnit.MILLISECONDS) + .until { + try { + Socket(host, port).use { true } + } catch (_: Exception) { + false + } + } + true +} catch (_: Exception) { + false +} + +/** + * Stops the given process. + * + * Attempts graceful shutdown first, then forces termination if necessary. + */ +public fun stopProcess(process: Process, wait: Duration = 1.seconds) { + process.destroy() + if (!process.waitFor(wait.inWholeMilliseconds, TimeUnit.MILLISECONDS)) { + process.destroyForcibly() + } +} + +/** + * Finds an available port by opening and closing a ServerSocket. + * + * @return An available port number + */ +public fun findFreePort(): Int { + ServerSocket(0).use { socket -> + return socket.localPort + } +} + +/** + * Creates a daemon thread that reads and logs process output. + * + * @param process The process to read from + * @param prefix Log prefix for output lines + * @return The created Thread (already started) + */ +public fun createProcessOutputReader(process: Process, prefix: String): Thread { + val thread = Thread { + try { + process.inputStream.bufferedReader().useLines { lines -> + lines.forEach { println("[$prefix] $it") } + } + } catch (e: Exception) { + println("Warning: Error reading process output: ${e.message}") + } + } + thread.isDaemon = true + return thread +} + +/** + * Creates a daemon thread that reads and logs process error output. + * + * @param process The process to read from + * @param prefix Log prefix for error lines + * @return The created Thread (already started) + */ +public fun createProcessErrorReader(process: Process, prefix: String): Thread { + val thread = Thread { + try { + process.errorStream.bufferedReader().useLines { lines -> + lines.forEach { println("[$prefix][err] $it") } + } + } catch (e: Exception) { + println("Warning: Error reading process error stream: ${e.message}") + } + } + thread.isDaemon = true + return thread +} diff --git a/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/testing.kt b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/testing.kt new file mode 100644 index 0000000..32f55c9 --- /dev/null +++ b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/testing.kt @@ -0,0 +1,28 @@ +package io.modelcontextprotocol.kotlin.test.utils + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import kotlin.coroutines.CoroutineContext +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +private val defaultTestDispatcher: CoroutineContext = Dispatchers.IO.limitedParallelism(100) +private val defaultTestTimeout: Duration = 5.seconds + +/** + * Executes a suspendable integration test block within a coroutine scope, applying a timeout constraint. + * + * @param timeout The maximum duration allowed for the test execution. Defaults to 5 seconds. + * @param context The coroutine context within which the test block is executed. + * Defaults to [Dispatchers.IO] with 100 parallel tasks. + * @param block The suspendable test logic to be executed. + */ +public fun runIntegrationTest( + context: CoroutineContext = defaultTestDispatcher, + timeout: Duration = defaultTestTimeout, + block: suspend CoroutineScope.() -> Unit, +): Unit = runBlocking(context) { + withTimeout(timeout, block) +}