diff --git a/.claude/skills/kdoc/SKILL.md b/.claude/skills/kdoc/SKILL.md deleted file mode 100644 index cfacfc3..0000000 --- a/.claude/skills/kdoc/SKILL.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -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 deleted file mode 100644 index 074e6f9..0000000 --- a/.claude/skills/mcp-docs/SKILL.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -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/.github/dependabot.yml b/.github/dependabot.yml index b951eaa..8d02aac 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,109 +1,20 @@ -# 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 +# Configuration options: +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#about-the-dependabotyml-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 67770a7..41e15f3 100644 --- a/.github/workflows/apidocs.yaml +++ b/.github/workflows/apidocs.yaml @@ -24,27 +24,30 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v6 + with: + submodules: true # Fetch Hugo themes (true OR recursive) + fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@v6 + uses: actions/setup-java@v5 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRIBUTION }} cache: gradle - name: Setup Gradle - uses: gradle/actions/setup-gradle@v6.3.0 + uses: gradle/actions/setup-gradle@v5 - name: Generate Dokka Site run: |- - ./gradlew knit docs:classes docs:knitCheck dokkaGenerate + ./gradlew clean knit docs:classes docs:knitCheck dokkaGenerate - name: Upload artifact - uses: actions/upload-pages-artifact@v5 + uses: actions/upload-pages-artifact@v4 with: path: build/dokka/html - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v5 + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/automations.yml b/.github/workflows/automations.yml index 60a9d3b..4f88d37 100644 --- a/.github/workflows/automations.yml +++ b/.github/workflows/automations.yml @@ -27,7 +27,7 @@ jobs: name: Mark/unmark stale PRs and issues runs-on: ubuntu-latest steps: - - uses: actions/stale@v11 + - uses: actions/stale@v10 with: debug-only: ${{ env.DRY_RUN }} exempt-draft-pr: true @@ -66,7 +66,7 @@ jobs: name: Close stale PRs and issues runs-on: ubuntu-latest steps: - - uses: actions/stale@v11 + - uses: actions/stale@v10 with: debug-only: ${{ env.DRY_RUN }} only-labels: 'stale' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ff99581..2b769d1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,12 +13,14 @@ concurrency: cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} permissions: - contents: read + checks: write + pull-requests: write # only required if `comment: true` was enabled env: JAVA_VERSION: 21 JAVA_DISTRIBUTION: temurin - GRADLE_OPTS: "-Dorg.gradle.daemon=false" + JAVA_OPTS: "-Dfile.encoding=UTF-8 -Djava.awt.headless=true" + GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dkotlin.compiler.execution.strategy=in-process" jobs: build: @@ -34,26 +36,31 @@ jobs: include: - os: ubuntu-latest job-name: "Linux Tests" - gradle-tasks: "build" + gradle-tasks: "build ktlintCheck koverLog koverHtmlReport koverXmlReport publishToMavenLocal -Pversion=1-SNAPSHOT" + max-workers: 3 + + - os: windows-latest + job-name: "Windows Tests" + gradle-tasks: "jvmTest -x :integration-test:jvmTest" max-workers: 3 - os: macos-latest job-name: "iOS Simulator tests" - gradle-tasks: "iosSimulatorArm64Test" + gradle-tasks: "linkIosSimulatorArm64 iosSimulatorArm64Test" max-workers: 2 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v6 - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@v6 + uses: actions/setup-java@v5 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRIBUTION }} - name: Set up Node.js if: matrix.os == 'ubuntu-latest' - uses: actions/setup-node@v7 + uses: actions/setup-node@v6 with: node-version: '22' # increase only after https://github.com/nodejs/node/issues/56645 will be fixed @@ -63,30 +70,24 @@ jobs: run: npm ci - name: Setup Gradle - uses: gradle/actions/setup-gradle@v6.3.0 + uses: gradle/actions/setup-gradle@v5 with: validate-wrappers: true add-job-summary: 'always' cache-read-only: ${{ github.ref != 'refs/heads/main' }} + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} gradle-home-cache-includes: | caches notifications + sdks ../.konan/** - name: ${{ matrix.job-name }} run: |- - ./gradlew ${{ matrix.gradle-tasks }} --max-workers ${{ matrix.max-workers }} --continue --rerun-tasks - - - name: Coverage Report - if: ${{ success() && matrix.os == 'ubuntu-latest' }} - run: ./gradlew koverLog koverHtmlReport koverXmlReport - - - name: Publish to Maven Local - if: ${{ success() && matrix.os == 'ubuntu-latest' }} - run: ./gradlew publishToMavenLocal -Pversion=1-SNAPSHOT + ./gradlew ${{ matrix.gradle-tasks }} -x :conformance-test:test --max-workers ${{ matrix.max-workers }} --continue --rerun-tasks --configure-on-demand --parallel - name: Upload Reports - if: failure() + if: always() uses: actions/upload-artifact@v7 with: name: reports-build-${{ matrix.os }} @@ -99,16 +100,34 @@ jobs: with: annotate_only: true detailed_summary: true + flaky_summary: true group_suite: true include_empty_in_summary: false + include_time_in_summary: true report_paths: '**/test-results/**/TEST-*.xml' truncate_stack_traces: false + - name: Upload coverage reports to Codecov + if: ${{ !cancelled() && matrix.os == 'ubuntu-latest' }} + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + report_type: coverage + directory: ./build/reports/kover + files: report.xml + + - name: Upload test results to Codecov + if: ${{ !cancelled() && matrix.os == 'ubuntu-latest' }} + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + report_type: test_results + - 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 . + cd ~/.m2/repository && zip -r $GITHUB_WORKSPACE/local-maven-repository.zip . - name: Upload Maven Local Repository if: ${{ success() && matrix.os == 'ubuntu-latest' }} @@ -126,20 +145,21 @@ jobs: contents: read steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v6 - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@v6 + uses: actions/setup-java@v5 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRIBUTION }} - name: Setup Gradle - uses: gradle/actions/setup-gradle@v6.3.0 + uses: gradle/actions/setup-gradle@v5 with: validate-wrappers: true add-job-summary: 'always' cache-read-only: ${{ github.ref != 'refs/heads/main' }} + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} gradle-home-cache-includes: | caches notifications @@ -148,36 +168,35 @@ jobs: - name: Run Knit run: |- - ./gradlew knit docs:classes docs:knitCheck --max-workers 3 --continue --rerun-tasks + ./gradlew knit docs:classes docs:knitCheck -x :conformance-test:test --max-workers 3 --continue --rerun-tasks --configure-on-demand --parallel 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 + env: + JAVA_OPTS: "-Xmx8g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6g" steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v6 - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@v6 + uses: actions/setup-java@v5 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRIBUTION }} - name: Setup Gradle - uses: gradle/actions/setup-gradle@v6.3.0 + uses: gradle/actions/setup-gradle@v5 with: add-job-summary: 'always' cache-read-only: true @@ -195,8 +214,8 @@ jobs: - name: Extract Maven Local Repository run: | - mkdir -p ~/.m2/repository/io/modelcontextprotocol - unzip -o local-maven-repository.zip -d ~/.m2/repository/io/modelcontextprotocol + mkdir -p ~/.m2/repository + unzip -o local-maven-repository.zip -d ~/.m2/repository - name: "Build Sample: ${{ matrix.sample }}" working-directory: ./samples/${{ matrix.sample }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2e7c58f..4657da9 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,14 +1,16 @@ name: "CodeQL Advanced" on: + push: + branches: [ "main" ] pull_request: branches: [ "main" ] schedule: - - cron: '0 7 * * 1' # Monday 8:00 CET (7:00 UTC) + - cron: '0 4 * * 0' concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} env: JAVA_VERSION: 21 @@ -16,42 +18,64 @@ env: jobs: analyze: - name: Analyze + name: Analyze (${{ matrix.language }}) 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: + fail-fast: false + matrix: + language: [ java-kotlin ] + steps: - name: Checkout repository - uses: actions/checkout@v7 + uses: actions/checkout@v6 + with: + fetch-depth: 0 - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@v6 + uses: actions/setup-java@v5 with: distribution: ${{ env.JAVA_DISTRIBUTION }} java-version: ${{ env.JAVA_VERSION }} + # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v4.38.0 + uses: github/codeql-action/init@v4 with: - languages: java-kotlin + languages: ${{ matrix.language }} build-mode: manual - name: Setup Gradle - uses: gradle/actions/setup-gradle@v6.3.0 + uses: gradle/actions/setup-gradle@v5 with: validate-wrappers: true cache-read-only: true - name: Build Kotlin sources run: | - ./gradlew compileKotlinJvm :conformance-test:compileKotlin \ + ./gradlew \ + :kotlin-sdk-core:compileKotlinJvm \ + :kotlin-sdk-client:compileKotlinJvm \ + :kotlin-sdk-server:compileKotlinJvm \ + :kotlin-sdk:compileKotlinJvm \ + :integration-test:compileKotlinJvm \ + :conformance-test:testClasses \ + detekt \ -Pkotlin.incremental=false \ - --no-daemon --rerun-tasks --stacktrace + --no-daemon --stacktrace --rerun-tasks - name: Analyze - uses: github/codeql-action/analyze@v4.38.0 + uses: github/codeql-action/analyze@v4 with: - category: '/language:java-kotlin' + category: '/language:${{ matrix.language }}' diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index b3e3935..1c9ba19 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -7,166 +7,67 @@ on: 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 }}" + run-conformance: + runs-on: ${{ matrix.os }} + name: Run Conformance Tests on ${{ matrix.os }} timeout-minutes: 20 + env: + JAVA_OPTS: "-Xmx8g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6g" strategy: fail-fast: false matrix: - scenario: - - initialize - - tools_call - - elicitation-sep1034-client-defaults - - sse-retry + include: + - os: ubuntu-latest + max-workers: 3 + - os: windows-latest + max-workers: 3 + - os: macos-latest + max-workers: 2 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v6 - - name: Set up JDK - uses: actions/setup-java@v6 + - name: Set up JDK 21 + uses: actions/setup-java@v5 with: - java-version: ${{ env.JAVA_VERSION }} - distribution: ${{ env.JAVA_DISTRIBUTION }} + java-version: '21' + distribution: 'temurin' + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '22' # increase only after https://github.com/nodejs/node/issues/56645 will be fixed + + - name: Setup Conformance Tests + working-directory: conformance-test + run: |- + npm install -g @modelcontextprotocol/conformance@0.1.8 - name: Setup Gradle - uses: gradle/actions/setup-gradle@v6.3.0 + uses: gradle/actions/setup-gradle@v5 with: + add-job-summary: 'always' cache-read-only: ${{ github.ref != 'refs/heads/main' }} + gradle-home-cache-includes: | + caches + notifications + sdks + ../.konan/** - - name: Build - run: ./gradlew :conformance-test:installDist + - name: Run Conformance Tests + run: |- + ./gradlew :conformance-test:test --no-daemon --max-workers ${{ matrix.max-workers }} - - name: Run conformance tests - uses: modelcontextprotocol/conformance@v0.1.16 + - name: Upload Conformance Results + if: always() + uses: actions/upload-artifact@v7 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 + name: conformance-results-${{ matrix.os }} + path: conformance-test/results/ diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml deleted file mode 100644 index e7e94c3..0000000 --- a/.github/workflows/dependabot-auto-merge.yml +++ /dev/null @@ -1,23 +0,0 @@ -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 f01f9c8..5acfce8 100644 --- a/.github/workflows/gradle-publish.yml +++ b/.github/workflows/gradle-publish.yml @@ -1,3 +1,10 @@ +# 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: @@ -10,30 +17,39 @@ jobs: runs-on: macos-latest environment: release env: - GRADLE_OPTS: "-Dorg.gradle.daemon=false" + JAVA_OPTS: "-Xmx4g -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=768m -Djava.awt.headless=true" + GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dkotlin.compiler.execution.strategy=in-process" permissions: contents: write packages: write steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v6 - name: Set up JDK 21 - uses: actions/setup-java@v6 + uses: actions/setup-java@v5 with: java-version: '21' distribution: 'temurin' - - 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: Setup Gradle - uses: gradle/actions/setup-gradle@v6.3.0 + uses: gradle/actions/setup-gradle@v5 - - name: Check - run: ./gradlew check --no-configuration-cache + - name: Clean Build with Gradle + run: ./gradlew clean build -x :conformance-test:test --rerun-tasks --max-workers 2 --continue --configure-on-demand --parallel + + - name: Publish Test Report + uses: mikepenz/action-junit-report@v6 + if: ${{ !cancelled() }} + with: + annotate_only: true + detailed_summary: true + flaky_summary: true + group_suite: true + include_empty_in_summary: false + include_time_in_summary: true + report_paths: '**/test-results/**/TEST-*.xml' + truncate_stack_traces: false - name: Publish to Maven Central Portal id: publish diff --git a/.github/workflows/samples.yml b/.github/workflows/samples.yml index 7463a9e..cdd98bb 100644 --- a/.github/workflows/samples.yml +++ b/.github/workflows/samples.yml @@ -19,7 +19,6 @@ concurrency: env: JAVA_VERSION: 21 JAVA_DISTRIBUTION: temurin - GRADLE_OPTS: "-Dorg.gradle.daemon=false" jobs: build: @@ -30,22 +29,23 @@ jobs: sample: - kotlin-mcp-client - kotlin-mcp-server - - simple-streamable-server - weather-stdio-server name: Build Sample (${{ matrix.sample }}) timeout-minutes: 10 + env: + JAVA_OPTS: "-Xmx8g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6g" steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v6 - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@v6 + uses: actions/setup-java@v5 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRIBUTION }} - name: Setup Gradle - uses: gradle/actions/setup-gradle@v6.3.0 + uses: gradle/actions/setup-gradle@v5 with: validate-wrappers: true add-job-summary: 'always' @@ -53,10 +53,10 @@ jobs: - name: "Build Sample: ${{ matrix.sample }}" working-directory: ./samples/${{ matrix.sample }} - run: ./gradlew build + run: ./gradlew --no-daemon build - name: Upload Reports - if: failure() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 with: name: reports-samples-${{ matrix.sample }} diff --git a/.idea/detekt.xml b/.idea/detekt.xml new file mode 100644 index 0000000..d89f908 --- /dev/null +++ b/.idea/detekt.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/.idea/externalDependencies.xml b/.idea/externalDependencies.xml new file mode 100644 index 0000000..0a081df --- /dev/null +++ b/.idea/externalDependencies.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/.idea/ktlint-plugin.xml b/.idea/ktlint-plugin.xml new file mode 100644 index 0000000..e8bd90c --- /dev/null +++ b/.idea/ktlint-plugin.xml @@ -0,0 +1,7 @@ + + + + DISTRACT_FREE + DEFAULT + + \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 6ef38be..1646c55 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,82 +1,334 @@ # AGENTS: Development Guidelines for AI Contributors -This file provides guidance to AI coding agents working with code in this repository. +This document is for autonomous agents and AI copilots contributing code to this repository. +Follow these rules to keep changes safe, comprehensible, and easy to maintain. -## Project +## Repository Layout -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. +- `kotlin-sdk-core`: Core protocol types, transport abstractions, serialization utilities +- `kotlin-sdk-client`: Client transports (STDIO, SSE, StreamableHttp, WebSocket) +- `kotlin-sdk-server`: Server transports + Ktor integration (STDIO, SSE, WebSocket) +- `kotlin-sdk`: Umbrella module that aggregates all three above +- `test-utils`: Shared test utilities +- `integration-test`: Integration tests with TypeScript/other implementations +- `conformance-test`: Protocol conformance tests +- `samples/`: Sample projects (composite builds) +- Package structure: `io.modelcontextprotocol.kotlin.sdk.*` +- JVM 11+, Kotlin 2.2+ -## Build Commands +## Prime Directives + +1. **Tests first, always.** + - Before changing code, identify or add tests that express the desired behavior. + - Prefer readable, minimal tests over clever ones. Tests are documentation. +2. **Keep tests simple and explicit.** + - Arrange/Act/Assert structure; avoid hidden magic and overuse of helpers. + - Prefer concrete inputs/outputs; avoid randomness and time dependence. +3. **Uphold SOLID principles** in production code: + - Single Responsibility: each class/function should do one thing well. + - Open/Closed: extend via new code, avoid risky edits to stable code paths. + - Liskov Substitution: honor contracts; keep types substitutable. + - Interface Segregation: keep abstractions small and focused. + - Dependency Inversion: depend on abstractions, not concretions. +4. **Make the minimal change** that satisfies the tests and the issue. + Stay focused on a single concern and avoid "drive-by" changes. +5. **Never commit changes!** It's human who should verify and commit to git. +6. **Keep the build green.** Do not merge changes that break existing tests. +7. **Prefer clarity** over micro-optimizations and cleverness. +8. **Ask when uncertain.** If requirements are ambiguous, request clarification with a concise question. +9. **Write code with the quality of a Kotlin Champion.** +10. **Use MCP servers** like `jetbrains` and `intellij-index` to explore the codebase, for code navigation, analysis, + refactoring, and running tests. +11. **Use the Bash tool for terminal commands** instead of MCP terminal execution features. +12. **Suggest updates** to AGENTS.md/CLAUDE.md with best practices and guidelines. + +## Code Style + +### Kotlin + +- Avoid changing public API signatures. Run `./gradlew apiCheck` before every commit. +- **Explicit API mode** is strict: all public APIs must have explicit visibility modifiers and return types. +- Anything under an `internal` directory is not part of the public API and may change freely. +- Package structure follows: `io.modelcontextprotocol.kotlin.sdk.*` +- Follow Kotlin coding conventions +- Use the provided `.editorconfig` for consistent formatting +- Use Kotlin typesafe DSL builders where possible; prioritize fluent builder style over standard builder methods +- Prefer DSL builder style (method with lambda blocks) over constructors, if possible +- Use `val` for immutable properties and `var` for mutable; consider `lateinit var` instead of nullable types when + appropriate +- Use multi-dollar interpolation prefix for strings where applicable +- Use fully qualified imports instead of star imports +- Ensure backward compatibility when making changes + +## Testing Guidance + +### Test Organization + +- **Location**: `src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/` +- **Platform-specific**: `src/jvmTest/`, `src/jsTest/`, etc. +- **Naming**: Use function `Names with backticks`, e.g., `fun \`should return 200 OK\`()` +- **Structure**: Arrange/Act/Assert pattern +- **Documentation**: Keep tests self-documenting; don't add KDocs to tests. + +### Test Principles + +- Write comprehensive tests for new features. Always add tests for new features or bug fixes, even if not explicitly + requested. +- **Prioritize test readability** +- Avoid creating too many test methods; use parametrized tests when testing multiple similar scenarios +- When running tests on Kotlin Multiplatform projects, run JVM tests only unless asked for other platforms + +### Test Framework Stack + +- **kotlin-test**: Core test framework +- **Kotest assertions**: Use infix style (`shouldBe`, `shouldContain`) instead of `assertEquals`. + - Use `shouldMatchJson` from kotest-assertions-json for JSON validation. + - For nullable objects with nested properties, prefer `shouldNotBeNull { ... }` blocks: + ```kotlin + content.annotations shouldNotBeNull { + priority shouldBe 1.0 + } + ``` +- **mockk**: Mocking library +- **Ktor MockEngine**: For HTTP client mocking (`io.ktor:ktor-client-mock`) +- **Java tests**: Use JUnit5, Mockito, AssertJ core +- **Serialization test utilities** (`io.modelcontextprotocol.kotlin.test.utils`): + - `verifySerialization(value, json, expectedJson)` — serializes, asserts match, round-trips back; use for most serialization tests + - `verifyDeserialization(json, payload)` — deserializes from JSON, re-serializes, asserts match; returns the object for further assertions + - Always test both empty/null/omitted and non-null cases for nullable fields; `McpJson` has `explicitNulls = false` so null properties must be absent from JSON, not `null` + +### Kotest Patterns + +- Basic assertions: Use infix style (`shouldBe`, `shouldContain`) instead of `assertEquals`. + ```kotlin + result shouldBe expected + list shouldContain item + ``` +- Nullable Assertions + ```kotlin + // Preferred: lambda-style for multiple assertions + content.annotations shouldNotBeNull { + priority shouldBe 1.0 + } + ``` + +- JSON Assertions (kotest-assertions-json): + ```kotlin + // language=json + val expected = """ + { + "id": 123 + } + """.trimIndent() + actual shouldEqualJson expected + ``` + +- Soft Assertions + ```kotlin + // Use for multiple assertions on SAME subject + assertSoftly(subject) { + name shouldBe "test" + value shouldBe 42 + } + // DON'T use for different subjects or single assertions + ``` +- Clues (Use Sparingly) + ```kotlin + // Only when assertion is NOT obvious + withClue("Failed to parse response") { + result shouldNotBe null + } + ``` + +#### Avoiding `this.` in Lambda Blocks + +```kotlin +// Avoid explicit `this.` unless necessary for disambiguation +prop.shouldNotBeNull { + enum shouldHaveSize 2 // Preferred + this.enum shouldHaveSize 2 // Avoid +} +``` + +### Multiplatform Patterns + +- Use `expect`/`actual` pattern for platform-specific implementations in `utils.*` files. +- Test changes on JVM first, then verify platform-specific behavior if needed. +- Supported targets: JVM (11+), JS/Wasm, iOS, watchOS, tvOS. + +## Coding Guidance + +### Module Boundaries + +- Respect separation of concerns between modules +- Keep abstractions framework-agnostic +- Avoid leaking implementation details across module boundaries + +### Serialization + +- Use Kotlinx Serialization with explicit `@Serializable` annotations +- JSON config is defined in `jsonUtils.kt` as `McpJson` — use it consistently +- Register custom serializers in companion objects + +### Error Handling + +- Use sealed classes for result states +- Map errors to JSON-RPC error codes in protocol handlers +- Log errors using `io.github.oshai.kotlinlogging.KotlinLogging` +- Never log sensitive data (credentials, tokens) + +### Code Quality + +- Handle nullability and references as established patterns +- Keep changes small and reversible; prefer adding functions over editing many call sites +- Add focused comments **only** where intent is non-obvious +- Use `git mv` command when moving version-controlled files +- Never commit or push changes. This is a Human responsibility. + +## Workflow for AI Agents + +1. **Understand the issue** + - Summarize requirement in 1-2 sentences + - Identify affected files and tests; ask if unclear + +2. **Plan minimal changes** + - Use TaskCreate/TaskUpdate to track work, or EnterPlanMode for complex changes + - Document affected files and approach + +3. **Work with code** + - View, edit, and run tests through MCP tools + +4. **Start with tests** + - Update existing or add new tests in relevant modules + +5. **Implement the change** + - Prefer composition and small functions + - Avoid breaking public APIs without tests and discussion + +6. **Run tests locally** + ```bash + ./gradlew test + ./gradlew :kotlin-sdk-core:test # Specific module + ./gradlew :integration-test:test # Integration tests + ``` + +7. **Verify results** + - Use Kotest JSON matchers for schema validation + - Double-check the readability of code and tests + +8. **Document key decisions** + - Brief notes in markdown document, if requested + +## Build and Run 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 +# Build all modules +./gradlew build + +# Run all tests +./gradlew test + +# Run specific module tests +./gradlew :kotlin-sdk-core:test +./gradlew :kotlin-sdk-client:test +./gradlew :kotlin-sdk-server:test + +# Check API compatibility (required before commit) +./gradlew apiCheck + +# Update API dump (after intentional API changes) +./gradlew apiDump + +# Platform-specific tests +./gradlew jsTest +./gradlew wasmJsTest ``` -Run a single test class: -```bash -./gradlew :kotlin-sdk-server:jvmTest --tests "io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransportTest" -``` +## Definition of done -Run a single test method: -```bash -./gradlew :kotlin-sdk-core:jvmTest --tests "*.ProtocolTest.should preserve existing meta when adding progress token" -``` +- Tests for affected functionality exist +- All tests pass across all targets +- Changes are focused on a single concern. +- Code respects module boundaries +- `./gradlew apiCheck` passes +- Documentation updated for user-facing changes -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) -``` +## Documentation -## Module Structure +### General Guidelines -``` -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) -``` +- **Never make up documentation/facts** +- Update README files when adding new features +- Document API changes in the appropriate module +- **Keep documentation concise and straight to the point** +- Always verify documentation matches code; fix discrepancies +- Write tutorials in Markdown format in README.md -## Architecture +### KDoc Guidelines -Layered design: +- Properly document interfaces and abstract classes in production code +- Avoid KDocs on override functions to reduce verbosity +- Update KDocs when API changes +- Use references: `[SendMessageRequest]` instead of `SendMessageRequest` +- Add brief code examples to KDocs +- Add links to specifications with verified URLs; never add broken links -``` -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) -``` +### Module.md Files for Dokka -## Code Conventions +Each module must have a `Module.md` file at the module root for Dokka-generated API documentation. -- **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 +**Format Requirements** (per [Dokka specification](https://kotlinlang.org/docs/dokka-module-and-package-docs.html)): -## Testing +- Must start with `# Module module-name` (level 1 header) +- Package documentation uses `# Package package.name` (level 1 header) +- All other content uses level 2+ headers (`##`, `###`, etc.) -- **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 +**Content Guidelines**: -## Skills +- **Purpose**: Provide module-level overview in generated API docs +- **Detail level**: API documentation style (concise, focused on classes/interfaces) +- **Module section**: + - Brief module description (1-2 sentences) + - Platform support (format: "Multiplatform • Kotlin 2.2+" or "JVM only • Kotlin 2.2+") + - Key classes/components (use Dokka references like `[ClassName]`) + - Example (minimal, demonstrating primary usage) +- **Package sections**: Use `# Package package.name` headers with brief description +- **Optional sections**: Features, Limitations, Related Specifications +- **Style**: + - Use bullet points over prose + - Keep examples short (5-15 lines) + - Reference related modules using Dokka links + - Avoid duplicating README content -- `/mcp-docs` — fetches live MCP specification and docs; use when implementing or debugging MCP protocol features +**Example structure (markdown)**: + + # Module module-name + + Brief description. + + **Platform Support:** Multiplatform • Kotlin 2.2+ + + ## Key Classes + + - [MainClass] - what it does + - [HelperClass] - what it does + + ## Example + + ```kotlin + val example = MainClass() + ``` + + # Package package.name + + Package description. + + # Package another.package + + Another package description. + +## When to Ask for Help + +- Requirements conflict with existing tests or documentation +- The smallest change still requires altering core abstractions +- Unclear expected schema shape for a new feature—ask for a concrete, readable test case to anchor implementation diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 120000 index 47dc3e3..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -AGENTS.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/README.md b/README.md index aefadcf..30d1b40 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,14 @@ [![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) +[![Conformance Tests](https://github.com/modelcontextprotocol/kotlin-sdk/actions/workflows/conformance.yml/badge.svg)](https://github.com/modelcontextprotocol/kotlin-sdk/actions/workflows/conformance.yml) +[![codecov](https://codecov.io/github/modelcontextprotocol/kotlin-sdk/graph/badge.svg?token=O2CFHNSFYM)](https://codecov.io/github/modelcontextprotocol/kotlin-sdk) [![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) - +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/modelcontextprotocol/kotlin-sdk) 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 @@ -181,18 +183,15 @@ fun main(args: Array) = runBlocking { ### 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. +Create an MCP server that exposes a simple tool and runs on an embedded Ktor server with SSE transport: - ```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.mcpStreamableHttp +import io.modelcontextprotocol.kotlin.sdk.server.mcp import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult import io.modelcontextprotocol.kotlin.sdk.types.Implementation import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities @@ -201,6 +200,7 @@ 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( @@ -226,9 +226,9 @@ fun main(args: Array) { ) { request -> CallToolResult(content = listOf(TextContent("Hello, world!"))) } - + embeddedServer(CIO, host = "127.0.0.1", port = port) { - mcpStreamableHttp { + mcp { mcpServer } }.start(wait = true) @@ -243,7 +243,7 @@ You can run the server and then connect to it using the client or test with the npx -y @modelcontextprotocol/inspector ``` -In the inspector UI, connect to `http://localhost:3000/mcp`. +In the inspector UI, connect to `http://localhost:3000`. ## Core Concepts @@ -729,7 +729,7 @@ val client = Client( ) client.setRequestHandler(Method.Defined.SamplingCreateMessage) { request, _ -> - val content = request.messages.lastOrNull()?.content?.firstOrNull() + val content = request.messages.lastOrNull()?.content val prompt = if (content is TextContent) content.text else "your topic" CreateMessageResult( model = "gpt-4o-mini", @@ -766,10 +766,8 @@ CLI tooling that spawns a helper process. No networking setup is required. `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: +remote deployments and integrates nicely with proxies or service meshes. Both accept a `path` parameter (default: +`"/mcp"`) to mount the endpoint at any URL: - ```kotlin embeddedServer(CIO, port = 3000) { mcpStreamableHttp(path = "/api/mcp") { @@ -797,36 +794,16 @@ embeddedServer(CIO, port = 3000) { } }.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. +- **`Application.mcp { }`** — installs the SSE plugin automatically and registers MCP endpoints at `/`. - **`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: @@ -854,7 +831,6 @@ private class MyServer : fun main() { --> - ```kotlin embeddedServer(CIO, port = 3000) { install(SSE) @@ -865,7 +841,6 @@ embeddedServer(CIO, port = 3000) { } }.start(wait = true) ``` - @@ -905,12 +880,14 @@ allowing for easy testing of MCP functionality without the need for network setu ## 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. +| Scenario | Description | Example | +|--------------------------|-----------------------------------------------------------------|--------------------------------------------------------------------------| +| Streamable HTTP server | Full MCP server with prompts, resources, tools, completions | [samples/kotlin-mcp-server](./samples/kotlin-mcp-server) | +| STDIO weather server | Minimal STDIO transport server exposing weather info and alerts | [samples/weather-stdio-server](./samples/weather-stdio-server) | +| Interactive STDIO client | MCP client that connects over STDIO and pipes requests to LLMs | [samples/kotlin-mcp-client](./samples/kotlin-mcp-client) | +| Streamable HTTP client | MCP client demo in a runnable notebook | [samples/notebooks/McpClient.ipynb](./samples/notebooks/McpClient.ipynb) | ## 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) @@ -922,5 +899,4 @@ Please see the [contribution guide](CONTRIBUTING.md) and the [Code of conduct](C ## License -This project is licensed under Apache 2.0 for new contributions, with existing code under MIT—see the [LICENSE](LICENSE) -file for details. +This project is licensed under Apache 2.0 for new contributions, with existing code under MIT—see the [LICENSE](LICENSE) file for details. diff --git a/build.gradle.kts b/build.gradle.kts index 2c2dbe3..f4ddd44 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,8 @@ +import dev.detekt.gradle.extensions.FailOnSeverity + plugins { id("mcp.dokka") + alias(libs.plugins.detekt) alias(libs.plugins.ktlint) alias(libs.plugins.kover) } @@ -20,6 +23,13 @@ dependencies { subprojects { apply(plugin = "org.jlleitschuh.gradle.ktlint") apply(plugin = "org.jetbrains.kotlinx.kover") + apply(plugin = "dev.detekt") + + detekt { + config = files("$rootDir/config/detekt/detekt.yml") + buildUponDefaultConfig = true + failOnSeverity.set(FailOnSeverity.Error) + } } dokka { diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 4a057ce..84676a4 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -8,7 +8,6 @@ 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 deleted file mode 100644 index 3c1419e..0000000 --- a/buildSrc/src/main/kotlin/mcp.detekt.gradle.kts +++ /dev/null @@ -1,23 +0,0 @@ -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 98a0a5e..f3d4616 100644 --- a/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts +++ b/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts @@ -5,7 +5,6 @@ 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") @@ -16,8 +15,6 @@ plugins { kotlin { compilerOptions { - languageVersion = KotlinVersion.KOTLIN_2_1 - apiVersion = KotlinVersion.KOTLIN_2_1 freeCompilerArgs = listOf( "-Wextra", @@ -42,11 +39,18 @@ kotlin { forkEvery = 100 testLogging { exceptionFormat = TestExceptionFormat.SHORT + showStandardStreams = true events("failed") } systemProperty("kotest.output.ansi", "true") + reports { + junitXml.required.set(true) + junitXml.includeSystemOutLog.set(true) + junitXml.includeSystemErrLog.set(true) + } } } + macosX64() macosArm64() linuxX64() linuxArm64() @@ -57,3 +61,12 @@ kotlin { explicitApi = ExplicitApiMode.Strict jvmToolchain(21) } + +tasks.named("detekt").configure { + dependsOn( + "detektMainJvm", + "detektCommonMainSourceSet", + "detektTestJvm", + "detektCommonTestSourceSet", + ) +} diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..9135aae --- /dev/null +++ b/codecov.yml @@ -0,0 +1,51 @@ +# Codecov configuration for Mokksy and AI-Mocks +# https://docs.codecov.com/docs/codecov-yaml + +codecov: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + status: + project: + default: + target: auto + threshold: 1% + patch: + default: + target: auto + threshold: 1% + +comment: + layout: "header, diff, flags, components" + behavior: default + require_changes: true + +component_management: + default_rules: + statuses: + - type: project + target: auto + individual_components: + + - component_id: kotlin-sdk-core + name: SDK Core + paths: + - kotlin-sdk-core/** + + - component_id: kotlin-sdk-client + name: SDK Client + paths: + - kotlin-sdk-client/** + + - component_id: kotlin-sdk-server + name: SDK Server + paths: + - kotlin-sdk-server/** + + - component_id: kotlin-sdk-testing + name: SDK Testing + paths: + - kotlin-sdk-testing/** + diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index a82b404..8c19749 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -3,839 +3,18 @@ config: 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 + excludes: &testFolders + - '**/test/**' + - '**/commonTest/**' + - '**/jvmTest/**' + - '**/jsTest/**' + - '**/iosTest/**' 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 + excludes: *testFolders 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 + excludes: *testFolders -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 deleted file mode 100644 index 6628455..0000000 --- a/conformance-test/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/results/ diff --git a/conformance-test/README.md b/conformance-test/README.md deleted file mode 100644 index 9b827bc..0000000 --- a/conformance-test/README.md +++ /dev/null @@ -1,118 +0,0 @@ -# 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 index a96f4fb..1877d09 100644 --- a/conformance-test/build.gradle.kts +++ b/conformance-test/build.gradle.kts @@ -1,34 +1,39 @@ +import org.gradle.api.tasks.testing.logging.TestExceptionFormat + 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) + testImplementation(project(":kotlin-sdk")) + testImplementation(project(":test-utils")) + testImplementation(kotlin("test")) + testImplementation(libs.kotlin.logging) + testImplementation(libs.ktor.client.cio) + testImplementation(libs.ktor.server.cio) + testImplementation(libs.ktor.server.websockets) + testRuntimeOnly(libs.slf4j.simple) +} + +tasks.test { + useJUnitPlatform() + + testLogging { + events("passed", "skipped", "failed") + showStandardStreams = true + showExceptions = true + showCauses = true + showStackTraces = true + exceptionFormat = TestExceptionFormat.FULL + } + + doFirst { + systemProperty("test.classpath", classpath.asPath) + + println("\n" + "=".repeat(60)) + println("MCP CONFORMANCE TESTS") + println("=".repeat(60)) + println("These tests validate compliance with the MCP specification.") + println("=".repeat(60) + "\n") + } } diff --git a/conformance-test/conformance-baseline.yml b/conformance-test/conformance-baseline.yml deleted file mode 100644 index f349b8c..0000000 --- a/conformance-test/conformance-baseline.yml +++ /dev/null @@ -1,5 +0,0 @@ -# Conformance test baseline - expected failures -# Add entries here as tests are identified as known SDK limitations -server: [] - -client: [] diff --git a/conformance-test/detekt-baseline.xml b/conformance-test/detekt-baseline.xml new file mode 100644 index 0000000..fe120f5 --- /dev/null +++ b/conformance-test/detekt-baseline.xml @@ -0,0 +1,9 @@ + + + + + BracesOnWhenStatements:ConformanceServer.kt:HttpServerTransport$when + BracesOnWhenStatements:ConformanceTest.kt:ConformanceTest$when + ForbiddenComment:ConformanceTest.kt:ConformanceTest.Companion$// TODO: Fix + + diff --git a/conformance-test/run-conformance.sh b/conformance-test/run-conformance.sh deleted file mode 100755 index 279254e..0000000 --- a/conformance-test/run-conformance.sh +++ /dev/null @@ -1,194 +0,0 @@ -#!/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 deleted file mode 100644 index dba24ff..0000000 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceClient.kt +++ /dev/null @@ -1,199 +0,0 @@ -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 deleted file mode 100644 index 6297139..0000000 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceCompletions.kt +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index 6c591e4..0000000 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformancePrompts.kt +++ /dev/null @@ -1,114 +0,0 @@ -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 deleted file mode 100644 index 89d83f6..0000000 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceResources.kt +++ /dev/null @@ -1,122 +0,0 @@ -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 deleted file mode 100644 index 8a18957..0000000 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceServer.kt +++ /dev/null @@ -1,38 +0,0 @@ -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 deleted file mode 100644 index c81c98c..0000000 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt +++ /dev/null @@ -1,454 +0,0 @@ -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 deleted file mode 100644 index ca7abb3..0000000 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/InMemoryEventStore.kt +++ /dev/null @@ -1,46 +0,0 @@ -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 deleted file mode 100644 index 9a20681..0000000 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/JWTScenario.kt +++ /dev/null @@ -1,189 +0,0 @@ -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 deleted file mode 100644 index c59e8e0..0000000 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/authCodeFlow.kt +++ /dev/null @@ -1,154 +0,0 @@ -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 deleted file mode 100644 index a0d6d86..0000000 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/basicScenario.kt +++ /dev/null @@ -1,54 +0,0 @@ -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 deleted file mode 100644 index 8e84a15..0000000 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/clientRegistration.kt +++ /dev/null @@ -1,73 +0,0 @@ -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 deleted file mode 100644 index db4b7f0..0000000 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/crossAppAccessScenario.kt +++ /dev/null @@ -1,82 +0,0 @@ -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 deleted file mode 100644 index 73979db..0000000 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/discovery.kt +++ /dev/null @@ -1,48 +0,0 @@ -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 deleted file mode 100644 index e1c5419..0000000 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/pkce.kt +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index 4964f20..0000000 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/registration.kt +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index 33b69ce..0000000 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/scopeHandling.kt +++ /dev/null @@ -1,29 +0,0 @@ -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 deleted file mode 100644 index 0c79bb9..0000000 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/tokenExchange.kt +++ /dev/null @@ -1,174 +0,0 @@ -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 deleted file mode 100644 index 4bc75a0..0000000 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/utils.kt +++ /dev/null @@ -1,120 +0,0 @@ -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/test/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceClient.kt b/conformance-test/src/test/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceClient.kt new file mode 100644 index 0000000..09ea50b --- /dev/null +++ b/conformance-test/src/test/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceClient.kt @@ -0,0 +1,85 @@ +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.sse.SSE +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport +import io.modelcontextprotocol.kotlin.sdk.shared.Transport +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject + +private val logger = KotlinLogging.logger {} + +fun main(args: Array) { + require(args.isNotEmpty()) { + "Server URL must be provided as an argument" + } + + val serverUrl = args.last() + logger.info { "Connecting to test server at: $serverUrl" } + + val httpClient = HttpClient(CIO) { + install(SSE) + } + val transport: Transport = StreamableHttpClientTransport(httpClient, serverUrl) + + val client = Client( + clientInfo = Implementation( + name = "kotlin-conformance-client", + version = "1.0.0", + ), + ) + + var exitCode = 0 + + runBlocking { + try { + client.connect(transport) + logger.info { "✅ Connected to server successfully" } + + try { + val tools = client.listTools() + logger.info { "Available tools: ${tools.tools.map { it.name }}" } + + if (tools.tools.isNotEmpty()) { + val toolName = tools.tools.first().name + logger.info { "Calling tool: $toolName" } + + val result = client.callTool( + CallToolRequest( + params = CallToolRequestParams( + name = toolName, + arguments = buildJsonObject { + put("input", JsonPrimitive("test")) + }, + ), + ), + ) + logger.info { "Tool result: ${result.content}" } + } + } catch (e: Exception) { + logger.debug(e) { "Error during tool operations (may be expected for some scenarios)" } + } + + logger.info { "✅ Client operations completed successfully" } + } catch (e: Exception) { + logger.error(e) { "❌ Client failed" } + exitCode = 1 + } finally { + try { + transport.close() + } catch (e: Exception) { + logger.warn(e) { "Error closing transport" } + } + httpClient.close() + } + } + + kotlin.system.exitProcess(exitCode) +} diff --git a/conformance-test/src/test/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceServer.kt b/conformance-test/src/test/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceServer.kt new file mode 100644 index 0000000..b3331dd --- /dev/null +++ b/conformance-test/src/test/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceServer.kt @@ -0,0 +1,426 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance + +import io.github.oshai.kotlinlogging.KotlinLogging +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.ApplicationCall +import io.ktor.server.application.install +import io.ktor.server.cio.CIO +import io.ktor.server.engine.embeddedServer +import io.ktor.server.request.header +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.delete +import io.ktor.server.routing.get +import io.ktor.server.routing.post +import io.ktor.server.routing.routing +import io.ktor.server.websocket.WebSockets +import io.ktor.server.websocket.webSocket +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import io.modelcontextprotocol.kotlin.sdk.server.WebSocketMcpServerTransport +import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport +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.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.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeoutOrNull +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.buildJsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap + +private val logger = KotlinLogging.logger {} +private val serverTransports = ConcurrentHashMap() +private val jsonFormat = Json { ignoreUnknownKeys = true } + +private const val SESSION_CREATION_TIMEOUT_MS = 2000L +private const val REQUEST_TIMEOUT_MS = 10_000L +private const val MESSAGE_QUEUE_CAPACITY = 256 + +private fun isInitializeRequest(json: JsonElement): Boolean = + json is JsonObject && json["method"]?.jsonPrimitive?.contentOrNull == "initialize" + +@Suppress("CyclomaticComplexMethod", "LongMethod") +fun main(args: Array) { + val port = args.getOrNull(0)?.toIntOrNull() ?: 3000 + + logger.info { "Starting MCP Conformance Server on port $port" } + + embeddedServer(CIO, port = port, host = "127.0.0.1") { + install(WebSockets) + + routing { + webSocket("/ws") { + logger.info { "WebSocket connection established" } + val transport = WebSocketMcpServerTransport(this) + val server = createConformanceServer() + + try { + server.createSession(transport) + } catch (e: Exception) { + logger.error(e) { "Error in WebSocket session" } + throw e + } + } + + get("/mcp") { + val sessionId = call.request.header("mcp-session-id") + ?: run { + call.respond(HttpStatusCode.BadRequest, "Missing mcp-session-id header") + return@get + } + val transport = serverTransports[sessionId] + ?: run { + call.respond(HttpStatusCode.BadRequest, "Invalid mcp-session-id") + return@get + } + transport.stream(call) + } + + post("/mcp") { + val sessionId = call.request.header("mcp-session-id") + val requestBody = call.receiveText() + + logger.debug { "Received request with sessionId: $sessionId" } + logger.trace { "Request body: $requestBody" } + + val jsonElement = try { + jsonFormat.parseToJsonElement(requestBody) + } catch (e: Exception) { + logger.error(e) { "Failed to parse request body as JSON" } + call.respond( + HttpStatusCode.BadRequest, + jsonFormat.encodeToString( + JsonObject.serializer(), + buildJsonObject { + put("jsonrpc", "2.0") + put( + "error", + buildJsonObject { + put("code", -32700) + put("message", "Parse error: ${e.message}") + }, + ) + put("id", JsonNull) + }, + ), + ) + return@post + } + + val transport = sessionId?.let { serverTransports[it] } + if (transport != null) { + logger.debug { "Using existing transport for session: $sessionId" } + transport.handleRequest(call, jsonElement) + } else { + if (isInitializeRequest(jsonElement)) { + val newSessionId = UUID.randomUUID().toString() + logger.info { "Creating new session with ID: $newSessionId" } + + val newTransport = HttpServerTransport(newSessionId) + serverTransports[newSessionId] = newTransport + + val mcpServer = createConformanceServer() + call.response.header("mcp-session-id", newSessionId) + + val sessionReady = CompletableDeferred() + CoroutineScope(Dispatchers.IO).launch { + try { + mcpServer.createSession(newTransport) + sessionReady.complete(Unit) + } catch (e: Exception) { + logger.error(e) { "Failed to create session" } + serverTransports.remove(newSessionId) + newTransport.close() + sessionReady.completeExceptionally(e) + } + } + + val sessionCreated = withTimeoutOrNull(SESSION_CREATION_TIMEOUT_MS) { + sessionReady.await() + } + + if (sessionCreated == null) { + logger.error { "Session creation timed out" } + serverTransports.remove(newSessionId) + call.respond( + HttpStatusCode.InternalServerError, + jsonFormat.encodeToString( + JsonObject.serializer(), + buildJsonObject { + put("jsonrpc", "2.0") + put( + "error", + buildJsonObject { + put("code", -32000) + put("message", "Session creation timed out") + }, + ) + put("id", JsonNull) + }, + ), + ) + return@post + } + + newTransport.handleRequest(call, jsonElement) + } else { + logger.warn { "Invalid request: no session ID or not an initialization request" } + call.respond( + HttpStatusCode.BadRequest, + jsonFormat.encodeToString( + JsonObject.serializer(), + buildJsonObject { + put("jsonrpc", "2.0") + put( + "error", + buildJsonObject { + put("code", -32000) + put("message", "Bad Request: No valid session ID provided") + }, + ) + put("id", JsonNull) + }, + ), + ) + } + } + } + + delete("/mcp") { + val sessionId = call.request.header("mcp-session-id") + val transport = sessionId?.let { serverTransports[it] } + if (transport != null) { + logger.info { "Terminating session: $sessionId" } + serverTransports.remove(sessionId) + transport.close() + call.respond(HttpStatusCode.OK) + } else { + logger.warn { "Invalid session termination request: $sessionId" } + call.respond(HttpStatusCode.BadRequest, "Invalid or missing session ID") + } + } + } + }.start(wait = true) +} + +@Suppress("LongMethod") +private fun createConformanceServer(): Server { + val server = Server( + Implementation( + name = "kotlin-conformance-server", + version = "1.0.0", + ), + ServerOptions( + capabilities = ServerCapabilities( + tools = ServerCapabilities.Tools(listChanged = true), + resources = ServerCapabilities.Resources(subscribe = true, listChanged = true), + prompts = ServerCapabilities.Prompts(listChanged = true), + ), + ), + ) + + server.addTool( + name = "test-tool", + description = "A test tool for conformance testing", + inputSchema = ToolSchema( + properties = buildJsonObject { + put( + "input", + buildJsonObject { + put("type", "string") + put("description", "Test input parameter") + }, + ) + }, + required = listOf("input"), + ), + ) { request -> + val input = (request.params.arguments?.get("input") as? JsonPrimitive)?.content ?: "no input" + CallToolResult( + content = listOf(TextContent("Tool executed with input: $input")), + ) + } + + server.addResource( + uri = "test://test-resource", + name = "Test Resource", + description = "A test resource for conformance testing", + mimeType = "text/plain", + ) { request -> + ReadResourceResult( + contents = listOf( + TextResourceContents("Test resource content", request.params.uri, "text/plain"), + ), + ) + } + + server.addPrompt( + name = "test-prompt", + description = "A test prompt for conformance testing", + arguments = listOf( + PromptArgument( + name = "arg", + description = "Test argument", + required = false, + ), + ), + ) { + GetPromptResult( + messages = listOf( + PromptMessage( + role = Role.User, + content = TextContent("Test prompt content"), + ), + ), + description = "Test prompt description", + ) + } + + return server +} + +private class HttpServerTransport(private val sessionId: String) : AbstractTransport() { + private val logger = KotlinLogging.logger {} + private val pendingResponses = ConcurrentHashMap>() + private val messageQueue = Channel(MESSAGE_QUEUE_CAPACITY) + + suspend fun stream(call: ApplicationCall) { + logger.debug { "Starting SSE stream for session $sessionId" } + call.response.apply { + header("Cache-Control", "no-cache") + header("Connection", "keep-alive") + } + call.respondTextWriter(ContentType.Text.EventStream) { + try { + while (true) { + val msg = messageQueue.receiveCatching().getOrNull() ?: break + write("event: message\ndata: ${McpJson.encodeToString(msg)}\n\n") + flush() + } + } catch (e: Exception) { + logger.warn(e) { "SSE stream terminated for session $sessionId" } + } finally { + logger.debug { "SSE stream closed for session $sessionId" } + } + } + } + + suspend fun handleRequest(call: ApplicationCall, requestBody: JsonElement) { + try { + val message = McpJson.decodeFromJsonElement(requestBody) + logger.debug { "Handling ${message::class.simpleName}: $requestBody" } + + when (message) { + is JSONRPCRequest -> { + val idKey = when (val id = message.id) { + is RequestId.NumberId -> id.value.toString() + is RequestId.StringId -> id.value + } + val responseDeferred = CompletableDeferred() + pendingResponses[idKey] = responseDeferred + + _onMessage.invoke(message) + + val response = withTimeoutOrNull(REQUEST_TIMEOUT_MS) { responseDeferred.await() } + if (response != null) { + call.respondText(McpJson.encodeToString(response), ContentType.Application.Json) + } else { + pendingResponses.remove(idKey) + logger.warn { "Timeout for request $idKey" } + call.respondText( + McpJson.encodeToString( + JSONRPCError( + message.id, + RPCError(RPCError.ErrorCode.REQUEST_TIMEOUT, "Request timed out"), + ), + ), + ContentType.Application.Json, + ) + } + } + + else -> { + call.respond(HttpStatusCode.Accepted) + } + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + logger.error(e) { "Error handling request" } + if (!call.response.isCommitted) { + call.respondText( + McpJson.encodeToString( + JSONRPCError( + RequestId(0), + RPCError(RPCError.ErrorCode.INTERNAL_ERROR, "Internal error: ${e.message}"), + ), + ), + ContentType.Application.Json, + HttpStatusCode.InternalServerError, + ) + } + } + } + + override suspend fun start() { + logger.debug { "Started transport for session $sessionId" } + } + + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { + when (message) { + is JSONRPCResponse -> { + val idKey = when (val id = message.id) { + is RequestId.NumberId -> id.value.toString() + is RequestId.StringId -> id.value + } + pendingResponses.remove(idKey)?.complete(message) ?: run { + logger.warn { "No pending response for ID $idKey, queueing" } + messageQueue.send(message) + } + } + + else -> messageQueue.send(message) + } + } + + override suspend fun close() { + logger.debug { "Closing transport for session $sessionId" } + messageQueue.close() + pendingResponses.clear() + invokeOnCloseCallback() + } +} diff --git a/conformance-test/src/test/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTest.kt b/conformance-test/src/test/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTest.kt new file mode 100644 index 0000000..41c9c3c --- /dev/null +++ b/conformance-test/src/test/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTest.kt @@ -0,0 +1,343 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance + +import io.github.oshai.kotlinlogging.KotlinLogging +import io.modelcontextprotocol.kotlin.test.utils.NPX +import io.modelcontextprotocol.kotlin.test.utils.findFreePort +import io.modelcontextprotocol.kotlin.test.utils.startLogging +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.TestFactory +import org.junit.jupiter.api.TestInstance +import java.io.BufferedReader +import java.io.InputStreamReader +import java.lang.management.ManagementFactory +import java.net.HttpURLConnection +import java.net.URI +import java.util.concurrent.TimeUnit +import kotlin.io.path.createTempFile +import kotlin.properties.Delegates +import kotlin.test.fail + +private val logger = KotlinLogging.logger {} + +val processStderrLogger = KotlinLogging.logger(name = "stderr") +val processStdoutLogger = KotlinLogging.logger(name = "stdout") + +private const val CONFORMANCE_VERSION = "0.1.8" + +enum class TransportType { + SSE, + WEBSOCKET, +} + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ConformanceTest { + + private var serverProcess: Process? = null + private var serverPort: Int by Delegates.notNull() + private val serverErrorOutput = mutableListOf() + private val maxErrorLines = 500 + + companion object { + private val SERVER_SCENARIOS = listOf( + "server-initialize", + "tools-list", + "tools-call-simple-text", + "resources-list", + "prompts-list", + // TODO: Fix + // - resources-read-text + // - prompts-get-simple + ) + + private val CLIENT_SCENARIOS = listOf( + "initialize", + // TODO: Fix + // "tools-call", + ) + + private val SERVER_TRANSPORT_TYPES = listOf( + TransportType.SSE, + // TODO: Fix +// TransportType.WEBSOCKET, + ) + + private val CLIENT_TRANSPORT_TYPES = listOf( + TransportType.SSE, + TransportType.WEBSOCKET, + ) + + private const val DEFAULT_TEST_TIMEOUT_SECONDS = 30L + private const val DEFAULT_SERVER_STARTUP_TIMEOUT_SECONDS = 10 + private const val INITIAL_BACKOFF_MS = 50L + private const val MAX_BACKOFF_MS = 500L + private const val BACKOFF_MULTIPLIER = 1.5 + private const val CONNECTION_TIMEOUT_MS = 500 + private const val GRACEFUL_SHUTDOWN_SECONDS = 5L + private const val FORCE_SHUTDOWN_SECONDS = 2L + + private fun getRuntimeClasspath(): String = ManagementFactory.getRuntimeMXBean().classPath + + private fun getTestClasspath(): String = System.getProperty("test.classpath") ?: getRuntimeClasspath() + + private fun waitForServerReady( + url: String, + timeoutSeconds: Int = DEFAULT_SERVER_STARTUP_TIMEOUT_SECONDS, + ): Boolean { + val deadline = System.currentTimeMillis() + (timeoutSeconds * 1000) + var lastError: Exception? = null + var backoffMs = INITIAL_BACKOFF_MS + + while (System.currentTimeMillis() < deadline) { + try { + val connection = URI(url).toURL().openConnection() as HttpURLConnection + connection.requestMethod = "GET" + connection.connectTimeout = CONNECTION_TIMEOUT_MS + connection.readTimeout = CONNECTION_TIMEOUT_MS + connection.connect() + + val responseCode = connection.responseCode + connection.disconnect() + logger.debug { "Server responded with code: $responseCode" } + return true + } catch (e: Exception) { + lastError = e + Thread.sleep(backoffMs) + backoffMs = (backoffMs * BACKOFF_MULTIPLIER).toLong().coerceAtMost(MAX_BACKOFF_MS) + } + } + + logger.error { "Server did not start within $timeoutSeconds seconds. Last error: ${lastError?.message}" } + return false + } + } + + @BeforeAll + fun startServer() { + serverPort = findFreePort() + val serverUrl = "http://127.0.0.1:$serverPort/mcp" + + logger.info { "Starting conformance test server on port $serverPort" } + + val processBuilder = ProcessBuilder( + "java", + "-cp", + getRuntimeClasspath(), + "io.modelcontextprotocol.kotlin.sdk.conformance.ConformanceServerKt", + serverPort.toString(), + ) + + val process = processBuilder.start() + serverProcess = process + + // capture stderr in the background + Thread { + try { + BufferedReader(InputStreamReader(process.errorStream)).use { reader -> + reader.lineSequence().forEach { line -> + synchronized(serverErrorOutput) { + if (serverErrorOutput.size >= maxErrorLines) { + serverErrorOutput.removeAt(0) + } + serverErrorOutput.add(line) + } + logger.debug { "Server stderr: $line" } + } + } + } catch (e: Exception) { + logger.trace(e) { "Error reading server stderr" } + } + }.apply { + name = "server-stderr-reader" + isDaemon = true + }.start() + + logger.info { "Waiting for server to start..." } + val serverReady = waitForServerReady(serverUrl) + + if (!serverReady) { + val errorInfo = synchronized(serverErrorOutput) { + if (serverErrorOutput.isNotEmpty()) { + "\n\nServer error output:\n${serverErrorOutput.joinToString("\n")}" + } else { + "" + } + } + serverProcess?.destroyForcibly() + throw IllegalStateException( + "Server failed to start within $DEFAULT_SERVER_STARTUP_TIMEOUT_SECONDS seconds. " + + "Check if port $serverPort is available.$errorInfo", + ) + } + + logger.info { "Server started successfully at $serverUrl" } + } + + @AfterAll + fun stopServer() { + serverProcess?.also { process -> + logger.info { "Stopping conformance test server (PID: ${process.pid()})" } + + try { + process.destroy() + val terminated = process.waitFor(GRACEFUL_SHUTDOWN_SECONDS, TimeUnit.SECONDS) + + if (!terminated) { + logger.warn { "Server did not terminate gracefully, forcing shutdown..." } + process.destroyForcibly() + process.waitFor(FORCE_SHUTDOWN_SECONDS, TimeUnit.SECONDS) + } else { + logger.info { "Server stopped gracefully" } + } + } catch (e: Exception) { + logger.error(e) { "Error stopping server process" } + } finally { + serverProcess = null + } + } ?: logger.debug { "No server process to stop" } + } + + @TestFactory + fun `MCP Server Conformance Tests`(): List = SERVER_TRANSPORT_TYPES.flatMap { transportType -> + SERVER_SCENARIOS.map { scenario -> + DynamicTest.dynamicTest("Server [$transportType]: $scenario") { + runServerConformanceTest(scenario, transportType) + } + } + } + + @TestFactory + fun `MCP Client Conformance Tests`(): List = CLIENT_TRANSPORT_TYPES.flatMap { transportType -> + CLIENT_SCENARIOS.map { scenario -> + DynamicTest.dynamicTest("Client [$transportType]: $scenario") { + runClientConformanceTest(scenario, transportType) + } + } + } + + private fun runServerConformanceTest(scenario: String, transportType: TransportType) { + val serverUrl = when (transportType) { + TransportType.SSE -> { + "http://127.0.0.1:$serverPort/mcp" + } + + TransportType.WEBSOCKET -> { + "ws://127.0.0.1:$serverPort/ws" + } + } + + val processBuilder = ProcessBuilder( + NPX, + "@modelcontextprotocol/conformance@$CONFORMANCE_VERSION", + "server", + "--url", + serverUrl, + "--scenario", + scenario, + ) + + runConformanceTest("server", scenario, processBuilder, transportType) + } + + private fun runClientConformanceTest(scenario: String, transportType: TransportType) { + val testClasspath = getTestClasspath() + + // Create an argfile to avoid Windows command line length limits + val argFile = createTempFile(suffix = ".args").toFile() + argFile.deleteOnExit() + + val mainClass = when (transportType) { + TransportType.SSE -> { + argFile.writeText( + buildString { + appendLine("-cp") + appendLine(testClasspath) + appendLine("io.modelcontextprotocol.kotlin.sdk.conformance.ConformanceClientKt") + }, + ) + "http://127.0.0.1:$serverPort/mcp" + } + + TransportType.WEBSOCKET -> { + argFile.writeText( + buildString { + appendLine("-cp") + appendLine(testClasspath) + appendLine("io.modelcontextprotocol.kotlin.sdk.conformance.WebSocketConformanceClientKt") + }, + ) + "ws://127.0.0.1:$serverPort/ws" + } + } + + val clientCommand = listOf( + "java", + "@${argFile.absolutePath}", + mainClass, + ) + + val processBuilder = ProcessBuilder( + NPX, + "@modelcontextprotocol/conformance@$CONFORMANCE_VERSION", + "client", + "--command", + clientCommand.joinToString(" "), + "--scenario", + scenario, + ) + + runConformanceTest("client", scenario, processBuilder, transportType) + } + + private fun runConformanceTest( + type: String, + scenario: String, + processBuilder: ProcessBuilder, + transportType: TransportType, + ) { + val capitalizedType = type.replaceFirstChar { it.uppercase() } + logger.info { "Running $type conformance test [$transportType]: $scenario" } + + val timeoutSeconds = + System.getenv("CONFORMANCE_TEST_TIMEOUT_SECONDS")?.toLongOrNull() ?: DEFAULT_TEST_TIMEOUT_SECONDS + + val process = processBuilder.start() + + process.errorStream.startLogging( + logger = processStderrLogger, + name = "test(PID=${process.pid()})", + ) + process.inputStream.startLogging( + logger = processStdoutLogger, + name = "test(PID=${process.pid()})", + ) + + val completed = process.waitFor(timeoutSeconds, TimeUnit.SECONDS) + + if (!completed) { + logger.error { + "$capitalizedType conformance test [$transportType] '$scenario' timed out after $timeoutSeconds seconds" + } + process.destroyForcibly() + throw AssertionError( + "❌ $capitalizedType conformance test [$transportType] '$scenario' " + + "timed out after $timeoutSeconds seconds", + ) + } + + when (val exitCode = process.exitValue()) { + 0 -> logger.info { "✅ $capitalizedType conformance test [$transportType] '$scenario' passed!" } + + else -> { + logger.error { + "$capitalizedType conformance test [$transportType] '$scenario' failed with exit code: $exitCode" + } + fail( + "❌ $capitalizedType conformance test [$transportType] '$scenario' " + + "failed (exit code: $exitCode). Check test output above for details.", + ) + } + } + } +} diff --git a/conformance-test/src/test/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/WebSocketConformanceClient.kt b/conformance-test/src/test/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/WebSocketConformanceClient.kt new file mode 100644 index 0000000..f385ddd --- /dev/null +++ b/conformance-test/src/test/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/WebSocketConformanceClient.kt @@ -0,0 +1,105 @@ +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.websocket.WebSockets +import io.ktor.client.plugins.websocket.webSocket +import io.ktor.websocket.WebSocketSession +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.shared.MCP_SUBPROTOCOL +import io.modelcontextprotocol.kotlin.sdk.shared.WebSocketMcpTransport +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject + +private val logger = KotlinLogging.logger {} + +class WebSocketClientTransport(override val session: WebSocketSession) : WebSocketMcpTransport() { + override suspend fun initializeSession() { + logger.debug { "WebSocket client session initialized" } + } +} + +@Suppress("LongMethod") +fun main(args: Array) { + require(args.isNotEmpty()) { + "Server WebSocket URL must be provided as an argument" + } + + val serverUrl = args.last() + logger.info { "Connecting to WebSocket test server at: $serverUrl" } + + val httpClient = HttpClient(CIO) { + install(WebSockets) + } + + var exitCode = 0 + + runBlocking { + try { + httpClient.webSocket(serverUrl, request = { + headers.append("Sec-WebSocket-Protocol", MCP_SUBPROTOCOL) + }) { + val transport = WebSocketClientTransport(this) + + val client = Client( + clientInfo = Implementation( + name = "kotlin-conformance-client-websocket", + version = "1.0.0", + ), + ) + + try { + client.connect(transport) + logger.info { "✅ Connected to server successfully" } + + try { + val tools = client.listTools() + logger.info { "Available tools: ${tools.tools.map { it.name }}" } + + if (tools.tools.isNotEmpty()) { + val toolName = tools.tools.first().name + logger.info { "Calling tool: $toolName" } + + val result = client.callTool( + CallToolRequest( + params = CallToolRequestParams( + name = toolName, + arguments = buildJsonObject { + put("input", JsonPrimitive("test")) + }, + ), + ), + ) + logger.info { "Tool result: ${result.content}" } + } + } catch (e: Exception) { + logger.debug(e) { "Error during tool operations (may be expected for some scenarios)" } + } + + logger.info { "✅ Client operations completed successfully" } + } catch (e: Exception) { + logger.error(e) { "❌ Client failed" } + exitCode = 1 + } finally { + try { + transport.close() + } catch (e: Exception) { + logger.warn(e) { "Error closing transport" } + } + } + } + } catch (e: Exception) { + logger.error(e) { "❌ WebSocket connection failed" } + exitCode = 1 + } finally { + httpClient.close() + } + } + + kotlin.system.exitProcess(exitCode) +} diff --git a/conformance-test/src/test/resources/simplelogger.properties b/conformance-test/src/test/resources/simplelogger.properties new file mode 100644 index 0000000..c6f3b90 --- /dev/null +++ b/conformance-test/src/test/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 + +# Log level for specific packages or classes +org.slf4j.simpleLogger.log.io.ktor.server=DEBUG +org.slf4j.simpleLogger.log.stdout=TRACE +org.slf4j.simpleLogger.log.stderr=TRACE +org.slf4j.simpleLogger.log.io.modelcontextprotocol=DEBUG diff --git a/context7.json b/context7.json deleted file mode 100644 index 5fde250..0000000 --- a/context7.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "$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/build.gradle.kts b/docs/build.gradle.kts index 1857ef1..ac24979 100644 --- a/docs/build.gradle.kts +++ b/docs/build.gradle.kts @@ -6,8 +6,6 @@ plugins { dependencies { implementation(project(":kotlin-sdk")) implementation(libs.ktor.server.cio) - implementation(libs.ktor.serialization) - implementation(libs.ktor.server.content.negotiation) } tasks.matching { diff --git a/gradle.properties b/gradle.properties index a5655c3..45b067c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,11 +1,17 @@ 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 +version=0.9.0 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a8d034f..f86685d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,33 +1,33 @@ [versions] # plugins version -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" +atomicfu = "0.31.0" +binaryCompatibilityValidatorPlugin = "0.18.1" +detekt = "2.0.0-alpha.2" +dokka = "2.1.0" +kotlin = "2.2.21" +kover = "0.9.7" +ktlint = "14.0.1" knit = "0.5.1" -mavenPublish = "0.37.0" -netty = "4.2.18.Final" +mavenPublish = "0.36.0" +netty = "4.2.10.Final" +openapi-generator = "7.18.0" # libraries version awaitility = "4.3.0" -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" +collections-immutable = "0.4.0" +coroutines = "1.10.2" +kotest = "6.1.3" +kotlinx-io = "0.8.2" +ktor = "3.2.3" +logging = "7.0.14" +mockk = "1.14.9" +mokksy = "0.8.0" +serialization = "1.9.0" +slf4j = "2.0.17" +junit = "5.14.2" [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" } @@ -44,7 +44,6 @@ kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx- # 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" } @@ -64,9 +63,9 @@ kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-cor 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" } +mokksy = { group = "dev.mokksy", name = "mokksy", version.ref = "mokksy" } 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 @@ -74,7 +73,9 @@ ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = " ktor-server-cio = { group = "io.ktor", name = "ktor-server-cio", version.ref = "ktor" } [plugins] +detekt = { id = "dev.detekt", version.ref = "detekt" } kotlinx-binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binaryCompatibilityValidatorPlugin" } kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } knit = { id = "org.jetbrains.kotlinx.knit", version.ref = "knit" } +openapi-generator = { id = "org.openapi.generator", version.ref = "openapi-generator" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index eddabd2..61285a6 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 ad7845b..37f78a6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,9 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.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 249efbb..adff685 100755 --- a/gradlew +++ b/gradlew @@ -20,7 +20,7 @@ ############################################################################## # -# gradlew start up script for POSIX generated by Gradle. +# Gradle 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 gradlew +# ksh Gradle # # 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/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. diff --git a/gradlew.bat b/gradlew.bat index 8508ef6..e509b2d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -19,12 +19,12 @@ @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem gradlew startup script for Windows +@rem Gradle startup script for Windows @rem @rem ########################################################################## -@rem Set local scope for the variables, and ensure extensions are enabled -setlocal EnableExtensions +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal 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 -"%COMSPEC%" /c exit 1 +goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -65,18 +65,29 @@ 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 +goto fail :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 +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* -:exitWithErrorLevel -@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts -"%COMSPEC%" /c exit %ERRORLEVEL% +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/integration-test/build.gradle.kts b/integration-test/build.gradle.kts index 7a3a4a3..30afe9e 100644 --- a/integration-test/build.gradle.kts +++ b/integration-test/build.gradle.kts @@ -1,6 +1,5 @@ plugins { id("mcp.multiplatform") - id("mcp.detekt") } kotlin { diff --git a/integration-test/detekt-baseline-test.xml b/integration-test/detekt-baseline-test.xml new file mode 100644 index 0000000..710b729 --- /dev/null +++ b/integration-test/detekt-baseline-test.xml @@ -0,0 +1,31 @@ + + + + + AbstractClassCanBeConcreteClass:AbstractPromptIntegrationTest.kt:AbstractPromptIntegrationTest$AbstractPromptIntegrationTest + AbstractClassCanBeConcreteClass:AbstractResourceIntegrationTest.kt:AbstractResourceIntegrationTest$AbstractResourceIntegrationTest + AbstractClassCanBeConcreteClass:AbstractToolIntegrationTest.kt:AbstractToolIntegrationTest$AbstractToolIntegrationTest + AbstractClassCanBeConcreteClass:BaseTransportTest.kt:BaseTransportTest$BaseTransportTest + CyclomaticComplexMethod:AbstractToolIntegrationTest.kt:AbstractToolIntegrationTest$private fun setupCalculatorTool + ForbiddenComment:StdioClientTransportTest.kt:StdioClientTransportTest$// TODO: fix running on windows + InjectDispatcher:AbstractKotlinClientTsServerTest.kt:AbstractKotlinClientTsServerTest$IO + InjectDispatcher:AbstractPromptIntegrationTest.kt:AbstractPromptIntegrationTest$IO + InjectDispatcher:AbstractResourceIntegrationTest.kt:AbstractResourceIntegrationTest$IO + InjectDispatcher:AbstractToolIntegrationTest.kt:AbstractToolIntegrationTest$IO + InjectDispatcher:KotlinClientTsServerEdgeCasesTestSse.kt:KotlinClientTsServerEdgeCasesTestSse$IO + InjectDispatcher:KotlinClientTsServerEdgeCasesTestStdio.kt:KotlinClientTsServerEdgeCasesTestStdio$IO + InjectDispatcher:SseIntegrationTest.kt:SseIntegrationTest$IO + InjectDispatcher:StdioClientTransportTest.kt:StdioClientTransportTest$IO + InjectDispatcher:StreamableHttpIntegrationTest.kt:StreamableHttpIntegrationTest$IO + InjectDispatcher:TsEdgeCasesTestSse.kt:TsEdgeCasesTestSse$IO + InjectDispatcher:WebSocketIntegrationTest.kt:WebSocketIntegrationTest$IO + MatchingDeclarationName:PromptIntegrationTestSse.kt:SchemaPromptIntegrationTestSse : AbstractPromptIntegrationTest + SleepInsteadOfDelay:KotlinServerForTsClientSse.kt:KotlinServerForTsClient$sleep(500) + ThrowsCount:AbstractPromptIntegrationTest.kt:AbstractPromptIntegrationTest$override fun configureServer + UnusedPrivateProperty:KotlinClientTsServerTestSse.kt:KotlinClientTsServerTestSse$private val host = "localhost" + UseCheckOrError:ClientTest.kt:ClientTest.<no name provided>$throw IllegalStateException("Test error") + UseOrEmpty:AbstractToolIntegrationTest.kt:AbstractToolIntegrationTest$(request.params.arguments?.get("tags") as? JsonArray)?.mapNotNull { (it as? JsonPrimitive)?.content } ?: emptyList() + UseRequire:AbstractPromptIntegrationTest.kt:AbstractPromptIntegrationTest$throw IllegalArgumentException("Missing required argument: $argName") + VarCouldBeVal:KotlinClientTsServerEdgeCasesTestSse.kt:KotlinClientTsServerEdgeCasesTestSse$private lateinit var client: Client + + 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 index 099329d..0726fa8 100644 --- 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 @@ -1,30 +1,21 @@ 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 @@ -43,28 +34,20 @@ 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 kotlinx.serialization.json.putJsonObject import kotlin.coroutines.cancellation.CancellationException import kotlin.test.Test import kotlin.test.assertEquals @@ -73,7 +56,6 @@ 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 @@ -113,7 +95,7 @@ class ClientTest { ), options = ClientOptions( capabilities = ClientCapabilities( - sampling = ClientCapabilities.sampling, + sampling = EmptyJsonObject, ), ), ) @@ -159,7 +141,7 @@ class ClientTest { ), options = ClientOptions( capabilities = ClientCapabilities( - sampling = ClientCapabilities.sampling, + sampling = EmptyJsonObject, ), ), ) @@ -284,8 +266,8 @@ class ClientTest { client.connect(failingTransport) } - exception.code shouldBe -32600 - exception.message shouldBe "Invalid Request" + assertEquals(-32600, exception.code) + assertEquals("MCP error -32600: Invalid Request", exception.message) assertTrue(closed) } @@ -346,7 +328,7 @@ class ClientTest { val client = Client( clientInfo = Implementation(name = "test client", version = "1.0"), options = ClientOptions( - capabilities = ClientCapabilities(sampling = ClientCapabilities.sampling), + capabilities = ClientCapabilities(sampling = EmptyJsonObject), ), ) @@ -434,14 +416,13 @@ class ClientTest { clientInfo = Implementation(name = "test client without capability", version = "1.0"), options = ClientOptions( capabilities = ClientCapabilities(), + // enforceStrictCapabilities = true // TODO() ), ) - val (clientTransport2, serverTransport2) = InMemoryTransport.createLinkedPair() - listOf( - launch { clientWithoutCapability.connect(clientTransport2) }, - launch { server.createSession(serverTransport2) }, - ).joinAll() + 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 { @@ -546,15 +527,6 @@ class ClientTest { 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) @@ -580,7 +552,6 @@ class ClientTest { def.await() runCatching { job.cancel("Cancelled by test") } defCancel.await() - cancelledOnWire.await() defTimeOut.await() } @@ -619,9 +590,17 @@ class ClientTest { 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() + // 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()) } // Request with 1 msec timeout should fail immediately @@ -642,16 +621,18 @@ class ClientTest { ), options = ClientOptions( capabilities = ClientCapabilities( - sampling = ClientCapabilities.sampling, + sampling = EmptyJsonObject, ), ), ) client.setRequestHandler(Method.Defined.SamplingCreateMessage) { _, _ -> CreateMessageResult( - role = Role.Assistant, - content = TextContent(text = "Test response"), model = "test-model", + role = Role.Assistant, + content = TextContent( + text = "Test response", + ), ) } @@ -677,7 +658,7 @@ class ClientTest { val client = Client( clientInfo = Implementation(name = "test client", version = "1.0"), options = ClientOptions( - capabilities = ClientCapabilities(sampling = ClientCapabilities.sampling), + capabilities = ClientCapabilities(sampling = EmptyJsonObject), ), ) @@ -900,8 +881,8 @@ class ClientTest { ), ) - // The handler runs on Dispatchers.Default after concurrent dispatch is enabled; await the deferred. - val rootListChangedReceived = CompletableDeferred() + // Track notifications + var rootListChangedNotificationReceived = false val serverSessionResult = CompletableDeferred() @@ -920,13 +901,16 @@ class ClientTest { serverSession.setNotificationHandler( Method.Defined.NotificationsRootsListChanged, ) { - rootListChangedReceived.complete(Unit) + rootListChangedNotificationReceived = true CompletableDeferred(Unit) } client.sendRootsListChanged() - withTimeout(5.seconds) { rootListChangedReceived.await() } + assertTrue( + rootListChangedNotificationReceived, + "Notification should be sent when sendRootsListChanged is called", + ) } @Test @@ -967,7 +951,11 @@ class ClientTest { serverSession.createElicitation( message = "Please provide your GitHub username", requestedSchema = ElicitRequestParams.RequestedSchema( - properties = mapOf("name" to StringSchema()), + properties = buildJsonObject { + putJsonObject("name") { + put("type", "string") + } + }, required = listOf("name"), ), ) @@ -1064,14 +1052,18 @@ class ClientTest { Implementation(name = "test client", version = "1.0"), ClientOptions( capabilities = ClientCapabilities( - elicitation = ClientCapabilities.Elicitation(), + elicitation = EmptyJsonObject, ), ), ) val elicitationMessage = "Please provide your GitHub username" val requestedSchema = ElicitRequestParams.RequestedSchema( - properties = mapOf("name" to StringSchema()), + properties = buildJsonObject { + putJsonObject("name") { + put("type", "string") + } + }, required = listOf("name"), ) @@ -1082,8 +1074,7 @@ class ClientTest { client.setElicitationHandler { request -> assertEquals(elicitationMessage, request.params.message) - val formParams = request.params as ElicitRequestFormParams - assertEquals(requestedSchema, formParams.requestedSchema) + assertEquals(requestedSchema, request.params.requestedSchema) ElicitResult( action = elicitationResultAction, @@ -1123,455 +1114,4 @@ class ClientTest { 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/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt index 8abf75d..d82ff47 100644 --- a/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt +++ b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt @@ -12,10 +12,9 @@ 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 @@ -38,6 +37,7 @@ class SseTransportTest : BaseTransportTest() { } @Test + @Ignore // Ignored because it doesn’t work with wasm/js in Ktor 3.2.3 fun `should start then close cleanly`() = runTest { val server = embeddedServer(CIO, port = 0) { install(ServerSSE) @@ -58,12 +58,13 @@ class SseTransportTest : BaseTransportTest() { } try { - testTransportOpenClose(transport) + testTransportRead(transport) } finally { server.stopSuspend() } } + @Ignore @Test fun `should read messages`() = runTest { val server = embeddedServer(CIO, port = 0) { @@ -84,28 +85,20 @@ class SseTransportTest : BaseTransportTest() { } } - val client = Client( - clientInfo = Implementation(name = "test-client", version = "1.0"), - options = ClientOptions(), - ) - try { - withContext(Dispatchers.Default) { - client.connect(transport) - client.ping() - } + testTransportRead(transport) } 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 } + mcp { mcpServer } } }.startSuspend(wait = false) @@ -121,18 +114,9 @@ class SseTransportTest : BaseTransportTest() { } } - val client = Client( - clientInfo = Implementation(name = "test-client", version = "1.0"), - options = ClientOptions(), - ) - try { - withContext(Dispatchers.Default) { - client.connect(transport) - client.ping() - } + testTransportRead(transport) } finally { - client.close() server.stopSuspend() } } 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 index fc3c1d3..585af3f 100644 --- 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 @@ -7,13 +7,9 @@ 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 @@ -22,12 +18,10 @@ 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 @@ -115,43 +109,6 @@ class StdioClientTransportTest : BaseTransportTest() { } } - @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() @@ -190,22 +147,4 @@ class StdioClientTransportTest : BaseTransportTest() { 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 index ec42041..1e88558 100644 --- 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 @@ -42,6 +42,7 @@ import io.ktor.server.sse.SSE as ServerSSE /** * Base class for MCP authentication integration tests. */ +@Suppress("InjectDispatcher") abstract class AbstractAuthenticationTest { protected companion object { 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 deleted file mode 100644 index d8d64ec..0000000 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/ProtocolConcurrencyIntegrationTest.kt +++ /dev/null @@ -1,306 +0,0 @@ -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/integration-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 index fb97782..9380401 100644 --- a/integration-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,17 +1,11 @@ package io.modelcontextprotocol.kotlin.sdk.integration.kotlin 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 @@ -24,9 +18,7 @@ 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 @@ -696,78 +688,13 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { } } - val expectedMessage = "Prompt not found: 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) + val expectedMessage = "MCP error -32603: Prompt not found: non-existent-prompt" assertEquals( - listOf(null, "cursor-2", "cursor-3"), - receivedCursors.toList(), - "Client must forward each nextCursor into the next request", + RPCError.ErrorCode.INTERNAL_ERROR, + exception.code, + "Exception code should be INTERNAL_ERROR: ${RPCError.ErrorCode.INTERNAL_ERROR}", ) - 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) + assertEquals(expectedMessage, exception.message, "Unexpected error message for non-existent prompt") } } diff --git a/integration-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 index ab2a854..40b9552 100644 --- a/integration-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,16 +1,11 @@ package io.modelcontextprotocol.kotlin.sdk.integration.kotlin 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 @@ -23,10 +18,8 @@ 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 @@ -77,6 +70,23 @@ abstract class AbstractResourceIntegrationTest : KotlinTestBase() { ) } + server.addResource( + uri = testResourceUri, + name = testResourceName, + description = testResourceDescription, + mimeType = "text/plain", + ) { request -> + ReadResourceResult( + contents = listOf( + TextResourceContents( + text = testResourceContent, + uri = request.params.uri, + mimeType = "text/plain", + ), + ), + ) + } + server.addResource( uri = binaryResourceUri, name = binaryResourceName, @@ -203,11 +213,14 @@ abstract class AbstractResourceIntegrationTest : KotlinTestBase() { } } + val expectedMessage = "MCP error -32603: Resource not found: test://nonexistent.txt" + assertEquals( - RPCError.ErrorCode.RESOURCE_NOT_FOUND, + RPCError.ErrorCode.INTERNAL_ERROR, exception.code, - "Exception code should be RESOURCE_NOT_FOUND: ${RPCError.ErrorCode.RESOURCE_NOT_FOUND}", + "Exception code should be INTERNAL_ERROR: ${RPCError.ErrorCode.INTERNAL_ERROR}", ) + assertEquals(expectedMessage, exception.message, "Unexpected error message for invalid resource URI") } @Test @@ -299,76 +312,4 @@ 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/integration-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 index b802122..7da82cc 100644 --- a/integration-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 @@ -6,15 +6,8 @@ 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 @@ -31,9 +24,7 @@ import org.junit.jupiter.api.Test 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 @@ -800,75 +791,4 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { "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 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( - 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/streamablehttp/AbstractStreamableHttpIntegrationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/AbstractStreamableHttpIntegrationTest.kt index 535393e..0f6646e 100644 --- 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 @@ -1,14 +1,17 @@ package io.modelcontextprotocol.kotlin.sdk.integration.streamablehttp +import io.ktor.serialization.kotlinx.json.json +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.sse.Heartbeat +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions import io.modelcontextprotocol.kotlin.sdk.server.mcpStreamableHttp import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult import io.modelcontextprotocol.kotlin.sdk.types.Implementation +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.Role @@ -18,10 +21,7 @@ import io.ktor.server.cio.CIO as ServerCIO open class AbstractStreamableHttpIntegrationTest { - suspend fun initTestServer( - name: String? = null, - sseHeartbeatConfig: (Heartbeat.() -> Unit)? = null, - ): StreamableHttpTestServer { + suspend fun initTestServer(name: String? = null): StreamableHttpTestServer { val mcpServer = Server( Implementation(name = name ?: DEFAULT_SERVER_NAME, version = VERSION), ServerOptions( @@ -56,7 +56,10 @@ open class AbstractStreamableHttpIntegrationTest { host = URL, port = PORT, ) { - mcpStreamableHttp(sseHeartbeatConfig = sseHeartbeatConfig) { mcpServer } + install(ContentNegotiation) { + json(McpJson) + } + mcpStreamableHttp { mcpServer } } return StreamableHttpTestServer( 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 deleted file mode 100644 index 598825f..0000000 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpClientTransportIntegrationTest.kt +++ /dev/null @@ -1,270 +0,0 @@ -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 deleted file mode 100644 index 8019557..0000000 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpHeartbeatIntegrationTest.kt +++ /dev/null @@ -1,174 +0,0 @@ -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/StreamableHttpSseReconnectTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpSseReconnectTest.kt deleted file mode 100644 index 751f8dd..0000000 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpSseReconnectTest.kt +++ /dev/null @@ -1,171 +0,0 @@ -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/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 index 29a6de7..aa47a70 100644 --- 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 @@ -6,7 +6,7 @@ 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.integration.typescript.sse.KotlinServerForTsClient import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport import io.modelcontextprotocol.kotlin.sdk.types.Implementation @@ -26,7 +26,7 @@ import java.util.concurrent.TimeUnit import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds -enum class TransportKind { HTTP, STDIO } +enum class TransportKind { SSE, STDIO } @Retry(times = 3) abstract class TsTestBase { @@ -43,10 +43,10 @@ abstract class TsTestBase { protected val typescriptDir = File(projectRoot, "src/jvmTest/typescript") @JvmStatic - private var sharedHttpServer: Process? = null + private var sharedSseServer: Process? = null @JvmStatic - private var sharedHttpPort: Int = 0 + private var sharedSsePort: Int = 0 init { installDependencies(typescriptDir) @@ -54,23 +54,23 @@ abstract class TsTestBase { @JvmStatic @Synchronized - protected fun getSharedHttpUrl(): String { - if (sharedHttpServer == null || !sharedHttpServer!!.isAlive) { - sharedHttpPort = io.modelcontextprotocol.kotlin.test.utils.findFreePort() + protected fun getSharedSseUrl(): String { + if (sharedSseServer == null || !sharedSseServer!!.isAlive) { + sharedSsePort = io.modelcontextprotocol.kotlin.test.utils.findFreePort() val server = TypeScriptServer(typescriptDir) - sharedHttpServer = server.startHttp(sharedHttpPort) - println("Shared TypeScript HTTP server started on port $sharedHttpPort") + sharedSseServer = server.startSse(sharedSsePort) + println("Shared TypeScript SSE server started on port $sharedSsePort") Runtime.getRuntime().addShutdownHook( Thread { - sharedHttpServer?.let { - println("Stopping shared TypeScript HTTP server") + sharedSseServer?.let { + println("Stopping shared TypeScript SSE server") io.modelcontextprotocol.kotlin.test.utils.stopProcess(it) } }, ) } - return "http://localhost:$sharedHttpPort/mcp" + return "http://localhost:$sharedSsePort/mcp" } @JvmStatic @@ -141,7 +141,7 @@ abstract class TsTestBase { println("$name stopped") } - // ===== HTTP client helpers ===== + // ===== SSE client helpers ===== protected suspend fun newClient(serverUrl: String): Client = HttpClient(CIO) { install(SSE) }.mcpStreamableHttp(serverUrl) 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 index 109eab4..7664abf 100644 --- 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 @@ -7,28 +7,28 @@ 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. + * This class provides methods to start TypeScript clients using SSE 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. + * Starts a TypeScript MCP client using SSE 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 + fun startSse(arguments: List, log: Boolean = true): Process { + val scriptPath = File(typescriptDir, "client/sse-client.ts").absolutePath val proc = TypeScriptRunner.run( typescriptDir = typescriptDir, scriptPath = scriptPath, arguments = arguments, env = emptyMap(), redirectErrorStream = false, - logPrefix = "TS-CLIENT-HTTP", + logPrefix = "TS-CLIENT-SSE", log = log, ) process = proc 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 index 411a212..579f96d 100644 --- 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 @@ -12,23 +12,23 @@ 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 + * - Start TypeScript servers using SSE 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 + * - `npx tsx server/sse-server.ts` for SSE transport * * Usage: * ```kotlin * val typescriptDir = File("kotlin-sdk-test/src/jvmTest/typescript") * val server = TypeScriptServer(typescriptDir) * - * // Start HTTP server + * // Start SSE server * val port = TypeScriptServer.findFreePort() - * val process = server.startHttp(port) + * val process = server.startSse(port) * * // Stop server * server.stop() @@ -38,27 +38,27 @@ class TypeScriptServer(private val typescriptDir: File) { private var process: Process? = null /** - * Starts a TypeScript MCP server using Streamable HTTP transport. + * Starts a TypeScript MCP server using SSE transport. * - * @param port The port number to bind the HTTP server to + * @param port The port number to bind the SSE server to * @return The started Process * @throws IllegalStateException if the server fails to start within timeout */ - fun startHttp(port: Int): Process { + fun startSse(port: Int): Process { killProcessOnPort(port) - val serverPath = File(typescriptDir, "server/http-server.ts").absolutePath + val serverPath = File(typescriptDir, "server/sse-server.ts").absolutePath val proc = TypeScriptRunner.run( typescriptDir = typescriptDir, scriptPath = serverPath, env = mapOf("MCP_PORT" to port.toString()), redirectErrorStream = true, - logPrefix = "TS-SERVER-HTTP", + logPrefix = "TS-SERVER-SSE", ) process = proc check(waitForPort(port = port, timeoutSeconds = 60)) { proc.destroyForcibly() - "TypeScript HTTP server did not become ready on localhost:$port within timeout" + "TypeScript SSE server did not become ready on localhost:$port within timeout" } return proc } 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 deleted file mode 100644 index 016aa1c..0000000 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinClientTsServerEdgeCasesTestHttp.kt +++ /dev/null @@ -1,14 +0,0 @@ -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/AbstractKotlinClientTsServerEdgeCasesTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerEdgeCasesTestSse.kt similarity index 82% rename from integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractKotlinClientTsServerEdgeCasesTest.kt rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerEdgeCasesTestSse.kt index d977bd0..356fd30 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractKotlinClientTsServerEdgeCasesTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerEdgeCasesTestSse.kt @@ -1,29 +1,53 @@ -package io.modelcontextprotocol.kotlin.sdk.integration.typescript +package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse 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 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.Test import org.junit.jupiter.api.Timeout import java.util.concurrent.TimeUnit import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds -abstract class AbstractKotlinClientTsServerEdgeCasesTest : TsTestBase() { +class KotlinClientTsServerEdgeCasesTestSse : TsTestBase() { - protected abstract suspend fun useClient(block: suspend (Client) -> T): T + override val transportKind = TransportKind.SSE + + private val serverUrl: String by lazy { getSharedSseUrl() } + + private lateinit var client: Client + + @AfterEach + fun tearDown() { + if (::client.isInitialized) { + try { + runBlocking { + withTimeout(3.seconds) { + client.close() + } + } + } catch (e: Exception) { + println("Warning: Error during client close: ${e.message}") + } + } + } @Test @Timeout(30, unit = TimeUnit.SECONDS) fun testNonExistentTool(): Unit = runBlocking(Dispatchers.IO) { - useClient { client -> + withClient(serverUrl) { client -> val nonExistentToolName = "non-existent-tool" val arguments = mapOf("name" to "TestUser") @@ -46,7 +70,7 @@ abstract class AbstractKotlinClientTsServerEdgeCasesTest : TsTestBase() { @Test @Timeout(30, unit = TimeUnit.SECONDS) fun testSpecialCharactersInArguments(): Unit = runBlocking(Dispatchers.IO) { - useClient { client -> + withClient(serverUrl) { client -> val specialChars = "!@#$%^&*()_+{}[]|\\:;\"'<>,.?/" val arguments = mapOf("name" to specialChars) @@ -67,7 +91,7 @@ abstract class AbstractKotlinClientTsServerEdgeCasesTest : TsTestBase() { @Test @Timeout(30, unit = TimeUnit.SECONDS) fun testLargePayload(): Unit = runBlocking(Dispatchers.IO) { - useClient { client -> + withClient(serverUrl) { client -> val largeName = "A".repeat(10 * 1024) val arguments = mapOf("name" to largeName) @@ -88,7 +112,7 @@ abstract class AbstractKotlinClientTsServerEdgeCasesTest : TsTestBase() { @Test @Timeout(60, unit = TimeUnit.SECONDS) fun testConcurrentRequests(): Unit = runBlocking(Dispatchers.IO) { - useClient { client -> + withClient(serverUrl) { client -> val concurrentCount = 5 val responses = coroutineScope { val results = (1..concurrentCount).map { i -> @@ -123,7 +147,7 @@ abstract class AbstractKotlinClientTsServerEdgeCasesTest : TsTestBase() { @Test @Timeout(30, unit = TimeUnit.SECONDS) fun testInvalidArguments(): Unit = runBlocking(Dispatchers.IO) { - useClient { client -> + withClient(serverUrl) { client -> val invalidArguments = mapOf( "name" to JsonObject(mapOf("nested" to JsonPrimitive("value"))), ) @@ -147,7 +171,7 @@ abstract class AbstractKotlinClientTsServerEdgeCasesTest : TsTestBase() { @Test @Timeout(30, unit = TimeUnit.SECONDS) fun testMultipleToolCalls(): Unit = runBlocking(Dispatchers.IO) { - useClient { client -> + withClient(serverUrl) { client -> repeat(10) { i -> val name = "SequentialClient$i" val arguments = mapOf("name" to name) @@ -155,7 +179,8 @@ abstract class AbstractKotlinClientTsServerEdgeCasesTest : TsTestBase() { val result = client.callTool("greet", arguments) assertNotNull(result, "Tool call result should not be null for call $i") - val textContent = result.content.firstOrNull { it is TextContent } as? TextContent + val callResult = result + val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent assertNotNull(textContent, "Text content should be present for call $i") assertEquals( 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/sse/KotlinClientTsServerTestSse.kt similarity index 75% rename from integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinClientTsServerTestHttp.kt rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerTestSse.kt index 7d919af..2006285 100644 --- 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/sse/KotlinClientTsServerTestSse.kt @@ -1,4 +1,4 @@ -package io.modelcontextprotocol.kotlin.sdk.integration.typescript.http +package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse import io.modelcontextprotocol.kotlin.sdk.client.Client import io.modelcontextprotocol.kotlin.sdk.integration.typescript.AbstractKotlinClientTsServerTest @@ -6,10 +6,11 @@ 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 +class KotlinClientTsServerTestSse : AbstractKotlinClientTsServerTest() { + override val transportKind = TransportKind.SSE - private val serverUrl: String by lazy { getSharedHttpUrl() } + private val host = "localhost" + private val serverUrl: String by lazy { getSharedSseUrl() } override suspend fun useClient(block: suspend (Client) -> T): T = withClient(serverUrl) { client -> try { diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinServerForTsClientHttp.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinServerForTsClientSse.kt similarity index 92% rename from integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinServerForTsClientHttp.kt rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinServerForTsClientSse.kt index ebd0499..97acc29 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinServerForTsClientHttp.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinServerForTsClientSse.kt @@ -1,4 +1,4 @@ -package io.modelcontextprotocol.kotlin.sdk.integration.typescript.http +package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.http.ContentType @@ -42,9 +42,8 @@ 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.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeoutOrNull import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement @@ -133,16 +132,14 @@ class KotlinServerForTsClient { call.response.header("mcp-session-id", newSessionId) - launch(Dispatchers.IO) { - try { + val serverThread = Thread { + runBlocking { mcpServer.createSession(transport) - } catch (e: Exception) { - logger.error(e) { "Failed to create MCP session" } - transport.ready.completeExceptionally(e) } } + serverThread.start() - transport.ready.await() + Thread.sleep(500) transport.handleRequest(call, jsonElement) } else { @@ -176,7 +173,9 @@ class KotlinServerForTsClient { logger.info { "Terminating session: $sessionId" } val transport = serverTransports[sessionId]!! serverTransports.remove(sessionId) - transport.close() + runBlocking { + transport.close() + } call.respond(HttpStatusCode.OK) } else { logger.warn { "Invalid session termination request: $sessionId" } @@ -314,10 +313,9 @@ 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 Event stream for session: $sessionId" } + logger.debug { "Starting SSE stream for session: $sessionId" } call.response.header("Cache-Control", "no-cache") call.response.header("Connection", "keep-alive") call.respondTextWriter(ContentType.Text.EventStream) { @@ -333,28 +331,29 @@ class HttpServerTransport(private val sessionId: String) : AbstractTransport() { flush() } } catch (e: Exception) { - logger.warn(e) { "Event stream terminated for session: $sessionId" } + logger.warn(e) { "SSE stream terminated for session: $sessionId" } } finally { - logger.debug { "Event stream closed for session: $sessionId" } + logger.debug { "SSE stream closed for session: $sessionId" } } } } suspend fun handleRequest(call: ApplicationCall, requestBody: JsonElement) { try { - logger.trace { "Handling request body: $requestBody" } + logger.info { "Handling request body: $requestBody" } val message = McpJson.decodeFromJsonElement(requestBody) - logger.debug { "Decoded message: $message" } + logger.info { "Decoded message: $message" } if (message is JSONRPCRequest) { val id = message.id.toString() - logger.debug { "Received request with ID: $id, method: ${message.method}" } + logger.info { "Received request with ID: $id, method: ${message.method}" } val responseDeferred = CompletableDeferred() pendingResponses[id] = responseDeferred + logger.info { "Created deferred response for ID: $id" } - logger.trace { "Invoking onMessage handler for ID: $id" } + logger.info { "Invoking onMessage handler" } _onMessage.invoke(message) - logger.trace { "onMessage handler completed for ID: $id" } + logger.info { "onMessage handler completed" } try { val response = withTimeoutOrNull(10000) { @@ -427,28 +426,29 @@ 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, options: TransportSendOptions?) { - logger.trace { "Sending message: $message" } + logger.info { "Sending message: $message" } if (message is JSONRPCResponse) { val id = message.id.toString() - logger.debug { "Completing response for request ID: $id" } + logger.info { "Sending response for request ID: $id" } val deferred = pendingResponses.remove(id) if (deferred != null) { + logger.info { "Found pending response for ID: $id, completing deferred" } deferred.complete(message) return } else { logger.warn { "No pending response found for ID: $id" } } } else if (message is JSONRPCRequest) { - logger.debug { "Sending request with ID: ${message.id}" } + logger.info { "Sending request with ID: ${message.id}" } } else if (message is JSONRPCNotification) { - logger.debug { "Sending notification: ${message.method}" } + logger.info { "Sending notification: ${message.method}" } } + logger.info { "Queueing message for next client request" } messageQueue.send(message) } diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/TsClientKotlinServerTestHttp.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsClientKotlinServerTestSse.kt similarity index 85% rename from integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/TsClientKotlinServerTestHttp.kt rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsClientKotlinServerTestSse.kt index eff91e5..dd43b70 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/TsClientKotlinServerTestHttp.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsClientKotlinServerTestSse.kt @@ -1,4 +1,4 @@ -package io.modelcontextprotocol.kotlin.sdk.integration.typescript.http +package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse import io.modelcontextprotocol.kotlin.sdk.integration.typescript.AbstractTsClientKotlinServerTest import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind @@ -7,9 +7,9 @@ import io.modelcontextprotocol.kotlin.test.utils.killProcessOnPort import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach -class TsClientKotlinServerTestHttp : AbstractTsClientKotlinServerTest() { +class TsClientKotlinServerTestSse : AbstractTsClientKotlinServerTest() { - override val transportKind = TransportKind.HTTP + override val transportKind = TransportKind.SSE private var port: Int = 0 private lateinit var serverUrl: String @@ -36,11 +36,11 @@ class TsClientKotlinServerTestHttp : AbstractTsClientKotlinServerTest() { } override fun runClient(vararg args: String): String = tsClient.use { client -> - val process = client.startHttp(listOf(serverUrl) + args.toList(), log = true) + val process = client.startSse(listOf(serverUrl) + args.toList(), log = true) val output = StringBuilder() process.inputStream.bufferedReader().useLines { lines -> lines.forEach { line -> - println("[TS-CLIENT-HTTP] $line") + println("[TS-CLIENT-SSE] $line") output.append(line).append("\n") } } diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/TsEdgeCasesTestHttp.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsEdgeCasesTestSse.kt similarity index 90% rename from integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/TsEdgeCasesTestHttp.kt rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsEdgeCasesTestSse.kt index 9326cc6..ee81ce2 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/TsEdgeCasesTestHttp.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsEdgeCasesTestSse.kt @@ -1,4 +1,4 @@ -package io.modelcontextprotocol.kotlin.sdk.integration.typescript.http +package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TsTestBase @@ -21,9 +21,9 @@ import kotlin.test.assertEquals import kotlin.test.assertTrue import kotlin.test.fail -class TsEdgeCasesTestHttp : TsTestBase() { +class TsEdgeCasesTestSse : TsTestBase() { - override val transportKind = TransportKind.HTTP + override val transportKind = TransportKind.SSE private var port: Int = 0 private lateinit var serverUrl: String @@ -55,7 +55,7 @@ class TsEdgeCasesTestHttp : TsTestBase() { @Test @Timeout(30, unit = TimeUnit.SECONDS) fun testInvalidURL() = runTest { - val nonExistentToolCommand = "npx tsx client/http-client.ts $serverUrl non-existent-tool" + val nonExistentToolCommand = "npx tsx client/sse-client.ts $serverUrl non-existent-tool" val nonExistentToolOutput = executeCommandAllowingFailure(nonExistentToolCommand, typescriptDir) assertTrue( @@ -63,7 +63,7 @@ class TsEdgeCasesTestHttp : TsTestBase() { "Client should handle non-existent tool gracefully", ) - val invalidUrlCommand = "npx tsx client/http-client.ts http://localhost:${port + 1000}/mcp greet TestUser" + val invalidUrlCommand = "npx tsx client/sse-client.ts http://localhost:${port + 1000}/mcp greet TestUser" val invalidUrlOutput = executeCommandAllowingFailure(invalidUrlCommand, typescriptDir) assertTrue( @@ -84,7 +84,7 @@ class TsEdgeCasesTestHttp : TsTestBase() { tempFile.deleteOnExit() val specialCharsContent = tempFile.readText() - val specialCharsCommand = "npx tsx client/http-client.ts $serverUrl greet \"$specialCharsContent\"" + val specialCharsCommand = "npx tsx client/sse-client.ts $serverUrl greet \"$specialCharsContent\"" val specialCharsOutput = executeCommand(specialCharsCommand, typescriptDir) assertTrue( @@ -109,7 +109,7 @@ class TsEdgeCasesTestHttp : TsTestBase() { tempFile.deleteOnExit() val largeNameContent = tempFile.readText() - val largePayloadCommand = "npx tsx client/http-client.ts $serverUrl greet \"$largeNameContent\"" + val largePayloadCommand = "npx tsx client/sse-client.ts $serverUrl greet \"$largeNameContent\"" val largePayloadOutput = executeCommand(largePayloadCommand, typescriptDir) tempFile.delete() @@ -157,11 +157,11 @@ class TsEdgeCasesTestHttp : TsTestBase() { } val commands = listOf( - "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\"", + "npx tsx client/sse-client.ts $serverUrl greet \"Client1\"", + "npx tsx client/sse-client.ts $serverUrl multi-greet \"Client2\"", + "npx tsx client/sse-client.ts $serverUrl greet \"Client3\"", + "npx tsx client/sse-client.ts $serverUrl", + "npx tsx client/sse-client.ts $serverUrl multi-greet \"Client5\"", ) coroutineScope { @@ -242,7 +242,7 @@ class TsEdgeCasesTestHttp : TsTestBase() { @Timeout(120, unit = TimeUnit.SECONDS) fun testRapidSequentialRequests() = runBlocking { val outputs = (1..10).map { i -> - val command = "npx tsx client/http-client.ts $serverUrl greet \"RapidClient$i\"" + val command = "npx tsx client/sse-client.ts $serverUrl greet \"RapidClient$i\"" val output = executeCommand(command, typescriptDir) assertTrue( 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 index b8c8e93..2bdd361 100644 --- 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 @@ -1,12 +1,173 @@ 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 +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 +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Timeout +import java.util.concurrent.TimeUnit +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue -class KotlinClientTsServerEdgeCasesTestStdio : AbstractKotlinClientTsServerEdgeCasesTest() { +class KotlinClientTsServerEdgeCasesTestStdio : TsTestBase() { override val transportKind = TransportKind.STDIO - override suspend fun useClient(block: suspend (Client) -> T): T = withClientStdio { c, _ -> block(c) } + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testNonExistentToolOverStdio(): Unit = runBlocking(Dispatchers.IO) { + withClientStdio { client: Client, _ -> + val nonExistentToolName = "non-existent-tool" + val arguments = mapOf("name" to "TestUser") + + val result = 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 + @Timeout(30, unit = TimeUnit.SECONDS) + fun testSpecialCharactersInArgumentsOverStdio(): Unit = runBlocking(Dispatchers.IO) { + withClientStdio { client: 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 textContent = result.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 testLargePayloadOverStdio(): Unit = runBlocking(Dispatchers.IO) { + withClientStdio { client: 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 + 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(30, unit = TimeUnit.SECONDS) + fun testConcurrentRequestsOverStdio(): Unit = runBlocking(Dispatchers.IO) { + withClientStdio { client: 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 textContent = result.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 testInvalidArgumentsOverStdio(): Unit = runBlocking(Dispatchers.IO) { + withClientStdio { client: Client, _ -> + val invalidArguments = mapOf( + "name" to JsonObject(mapOf("nested" to JsonPrimitive("value"))), + ) + + val result = client.callTool("greet", invalidArguments) + assertNotNull(result, "Tool call result should not be null") + + val callResult = result + assertTrue(callResult.isError ?: false, "isError should be true for invalid arguments") + + val textContent = callResult.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, _ -> + 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 textContent = result.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/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 index a1cf797..81a3b41 100644 --- 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 @@ -1,7 +1,5 @@ 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 @@ -19,9 +17,6 @@ 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() { @@ -61,7 +56,7 @@ class ClientConnectionLoggingTest : AbstractServerFeaturesTest() { @ParameterizedTest @EnumSource(LoggingLevel::class) fun `notification should filter logging messages below level`(minLevel: LoggingLevel): Unit = runBlocking { - val receivedMessages = CopyOnWriteArrayList() + val receivedMessages = mutableListOf() client.setNotificationHandler(Method.Defined.NotificationsMessage) { receivedMessages.add(it) CompletableDeferred(Unit) @@ -84,10 +79,8 @@ class ClientConnectionLoggingTest : AbstractServerFeaturesTest() { 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 shouldHaveSize expectedLevels.size receivedMessages.map { it.params.level } shouldBe expectedLevels } @@ -122,7 +115,7 @@ class ClientConnectionLoggingTest : AbstractServerFeaturesTest() { @ParameterizedTest @EnumSource(LoggingLevel::class) fun `sendLoggingMessage should filter messages below level`(minLevel: LoggingLevel): Unit = runBlocking { - val receivedMessages = CopyOnWriteArrayList() + val receivedMessages = mutableListOf() client.setNotificationHandler(Method.Defined.NotificationsMessage) { receivedMessages.add(it) CompletableDeferred(Unit) @@ -145,16 +138,14 @@ class ClientConnectionLoggingTest : AbstractServerFeaturesTest() { 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 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() + val receivedMessages = mutableListOf() client.setNotificationHandler(Method.Defined.NotificationsMessage) { receivedMessages.add(it) CompletableDeferred(Unit) @@ -176,7 +167,6 @@ class ClientConnectionLoggingTest : AbstractServerFeaturesTest() { } 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() } + 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 index 581e5ae..71f8129 100644 --- 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 @@ -7,9 +7,6 @@ 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 @@ -62,7 +59,6 @@ class ClientConnectionTest : AbstractServerFeaturesTest() { override fun getClientCapabilities(): ClientCapabilities = ClientCapabilities( roots = ClientCapabilities.Roots(listChanged = true), - elicitation = ClientCapabilities.Elicitation(url = EmptyJsonObject), ) private val sampleRoots = listOf(Root("file:///project", "Project Root")) @@ -81,8 +77,6 @@ class ClientConnectionTest : AbstractServerFeaturesTest() { val loggingMessage = onClientNotification(Method.Defined.NotificationsMessage) val resourceUpdated = onClientNotification(Method.Defined.NotificationsResourcesUpdated) - val elicitationComplete = - onClientNotification(Method.Defined.NotificationsElicitationComplete) init { onClientRequest(Method.Defined.RootsList) { @@ -115,12 +109,6 @@ class ClientConnectionTest : AbstractServerFeaturesTest() { 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() } } @@ -140,9 +128,6 @@ class ClientConnectionTest : AbstractServerFeaturesTest() { ), ) sendResourceUpdated(ResourceUpdatedNotification(ResourceUpdatedNotificationParams("test://res"))) - sendElicitationComplete( - ElicitationCompleteNotification(ElicitationCompleteNotificationParams(elicitationId = "elicit-123")), - ) cap.sessionId.complete(sessionId) } 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 deleted file mode 100644 index f161d8e..0000000 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingTest.kt +++ /dev/null @@ -1,251 +0,0 @@ -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 index 52f6908..1eb828f 100644 --- 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 @@ -1,6 +1,5 @@ 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 @@ -58,39 +57,6 @@ class ServerBulkFeaturesTest : AbstractServerFeaturesTest() { 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 @@ -123,25 +89,6 @@ class ServerBulkFeaturesTest : AbstractServerFeaturesTest() { 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 @@ -176,27 +123,6 @@ class ServerBulkFeaturesTest : AbstractServerFeaturesTest() { 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 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 index 44b1e42..d7ae46d 100644 --- 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 @@ -1,8 +1,5 @@ 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 @@ -113,26 +110,6 @@ class ServerPromptsTest : AbstractServerFeaturesTest() { 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 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 deleted file mode 100644 index 8405469..0000000 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourceTemplateTest.kt +++ /dev/null @@ -1,241 +0,0 @@ -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/ServerResourcesTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourcesTest.kt index a830788..fafc3c8 100644 --- 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 @@ -1,10 +1,7 @@ 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 @@ -118,24 +115,6 @@ class ServerResourcesTest : AbstractServerFeaturesTest() { ) } - @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 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 deleted file mode 100644 index 38b169a..0000000 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSessionInitializeTest.kt +++ /dev/null @@ -1,194 +0,0 @@ -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 index 9ac744b..3572082 100644 --- 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 @@ -11,7 +11,6 @@ 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 @@ -82,27 +81,6 @@ class ServerToolsNotificationTest : AbstractServerFeaturesTest() { } } - @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 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 index 1b3df91..c2e8201 100644 --- 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 @@ -1,8 +1,5 @@ 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 @@ -101,52 +98,6 @@ class ServerToolsTest : AbstractServerFeaturesTest() { 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 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 index 4d798ce..ea246db 100644 --- 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 @@ -6,6 +6,7 @@ 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.server.ServerSession import io.modelcontextprotocol.kotlin.sdk.types.Implementation import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest import io.modelcontextprotocol.kotlin.sdk.types.InitializeResult @@ -15,6 +16,9 @@ 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.CompletableDeferred +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlin.test.Test @@ -39,9 +43,18 @@ class ChannelTransportTest { clientInfo = Implementation(name = "test client", version = "1.0"), ) - val serverSession = server.createSession(serverTransport) - client.connect(clientTransport) + 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, diff --git a/integration-test/src/jvmTest/typescript/client/http-client.ts b/integration-test/src/jvmTest/typescript/client/sse-client.ts similarity index 67% rename from integration-test/src/jvmTest/typescript/client/http-client.ts rename to integration-test/src/jvmTest/typescript/client/sse-client.ts index 5956a37..0acc256 100644 --- a/integration-test/src/jvmTest/typescript/client/http-client.ts +++ b/integration-test/src/jvmTest/typescript/client/sse-client.ts @@ -2,14 +2,14 @@ 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 serverUrl = args[0] || 'http://localhost:3001/mcp'; const toolName = args[1]; const toolArgs = args.slice(2); -const PROTOCOL_VERSION = "2025-11-25"; +const PROTOCOL_VERSION = "2024-11-05"; async function main() { if (!toolName) { - console.log('Usage: npx tsx client/http-client.ts [server-url] [tool-args...]'); + console.log('Usage: npx tsx client/sse-client.ts [server-url] [tool-args...]'); console.log('Using default server URL:', serverUrl); console.log('Available utils will be listed after connection'); } @@ -22,10 +22,6 @@ async function main() { 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)); @@ -34,20 +30,37 @@ async function main() { 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) => t.name).join(', ')); + console.log('Available utils:', tools.map((t: { name: any; }) => t.name).join(', ')); if (!toolName) { + await client.close(); return; } - const tool = tools.find((t) => t.name === toolName); + const tool = tools.find((t: { name: string; }) => t.name === toolName); if (!tool) { - throw new Error(`Tool "${toolName}" not found`); + console.error(`Tool "${toolName}" not found`); + // @ts-ignore + process.exit(1); } - const toolArguments: Record = {}; + const toolArguments = {}; if (toolName === "greet" && toolArgs.length > 0) { toolArguments["name"] = toolArgs[0]; @@ -80,7 +93,8 @@ async function main() { } catch (error) { console.error('Error:', error); - process.exitCode = 1; + // @ts-ignore + process.exit(1); } finally { await client.close(); console.log('Disconnected from server'); @@ -89,5 +103,6 @@ async function main() { main().catch(error => { console.error('Unhandled error:', error); + // @ts-ignore process.exit(1); }); diff --git a/integration-test/src/jvmTest/typescript/client/stdio-client.ts b/integration-test/src/jvmTest/typescript/client/stdio-client.ts index a9c765c..82c25de 100644 --- a/integration-test/src/jvmTest/typescript/client/stdio-client.ts +++ b/integration-test/src/jvmTest/typescript/client/stdio-client.ts @@ -3,7 +3,7 @@ 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 = "2025-11-25"; +const PROTOCOL_VERSION = "2024-11-05"; async function main() { if (!toolName) { @@ -65,10 +65,6 @@ async function main() { 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(); @@ -77,6 +73,21 @@ async function main() { 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(', ')); diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index ffb53fc..fddbba4 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -8,23 +8,27 @@ "name": "mcp-typescript-test-sdk", "version": "1.0.0", "dependencies": { - "@modelcontextprotocol/sdk": "^1.30.0", + "@modelcontextprotocol/sdk": "^1.26.0", + "body-parser": "^2.2.2", "express": "^5.2.1", - "zod": "^4.6.2" + "uuid": "^13.0.0", + "zod": "^4.3.5" }, "devDependencies": { + "@eslint/js": "^9.39.2", "@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" + "@types/node": "^24.10.6", + "@types/uuid": "^10.0.0", + "cors": "^2.8.5", + "tsx": "^4.21.0", + "typescript": "^5.9.3" } }, "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==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", "cpu": [ "ppc64" ], @@ -39,9 +43,9 @@ } }, "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==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", "cpu": [ "arm" ], @@ -56,9 +60,9 @@ } }, "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==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", "cpu": [ "arm64" ], @@ -73,9 +77,9 @@ } }, "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==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", "cpu": [ "x64" ], @@ -90,9 +94,9 @@ } }, "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==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", "cpu": [ "arm64" ], @@ -107,9 +111,9 @@ } }, "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==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", "cpu": [ "x64" ], @@ -124,9 +128,9 @@ } }, "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==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", "cpu": [ "arm64" ], @@ -141,9 +145,9 @@ } }, "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==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", "cpu": [ "x64" ], @@ -158,9 +162,9 @@ } }, "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==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", "cpu": [ "arm" ], @@ -175,9 +179,9 @@ } }, "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==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", "cpu": [ "arm64" ], @@ -192,9 +196,9 @@ } }, "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==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", "cpu": [ "ia32" ], @@ -209,9 +213,9 @@ } }, "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==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", "cpu": [ "loong64" ], @@ -226,9 +230,9 @@ } }, "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==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", "cpu": [ "mips64el" ], @@ -243,9 +247,9 @@ } }, "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==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", "cpu": [ "ppc64" ], @@ -260,9 +264,9 @@ } }, "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==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", "cpu": [ "riscv64" ], @@ -277,9 +281,9 @@ } }, "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==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", "cpu": [ "s390x" ], @@ -294,9 +298,9 @@ } }, "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==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", "cpu": [ "x64" ], @@ -311,9 +315,9 @@ } }, "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==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", "cpu": [ "arm64" ], @@ -328,9 +332,9 @@ } }, "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==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", "cpu": [ "x64" ], @@ -345,9 +349,9 @@ } }, "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==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", "cpu": [ "arm64" ], @@ -362,9 +366,9 @@ } }, "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==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", "cpu": [ "x64" ], @@ -379,9 +383,9 @@ } }, "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==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", "cpu": [ "arm64" ], @@ -396,9 +400,9 @@ } }, "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==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", "cpu": [ "x64" ], @@ -413,9 +417,9 @@ } }, "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==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", "cpu": [ "arm64" ], @@ -430,9 +434,9 @@ } }, "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==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", "cpu": [ "ia32" ], @@ -447,9 +451,9 @@ } }, "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==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", "cpu": [ "x64" ], @@ -463,25 +467,38 @@ "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==", + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, "license": "MIT", "engines": { - "node": ">=20" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" }, "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==", + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", + "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", "license": "MIT", "dependencies": { - "@hono/node-server": "^1.19.9 || ^2.0.5", + "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", @@ -579,13 +596,13 @@ "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==", + "version": "24.10.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.6.tgz", + "integrity": "sha512-B8h60xgJMR/xmgyX9fncRzEW9gCxoJjdenUhke2v1JGOd/V66KopmWrLPXi5oUI4VuiGK+d+HlXJjDRZMj21EQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~8.9.0" + "undici-types": "~7.16.0" } }, "node_modules/@types/qs": { @@ -623,345 +640,12 @@ "@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" - ], + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", "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" - } + "license": "MIT" }, "node_modules/accepts": { "version": "2.0.0", @@ -1010,20 +694,20 @@ } }, "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==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", - "content-type": "^2.0.0", + "content-type": "^1.0.5", "debug": "^4.4.3", - "http-errors": "^2.0.1", - "iconv-lite": "^0.7.2", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", - "qs": "^6.15.2", - "raw-body": "^3.0.2", - "type-is": "^2.1.0" + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" @@ -1033,19 +717,6 @@ "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", @@ -1125,9 +796,9 @@ } }, "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "license": "MIT", "dependencies": { "object-assign": "^4", @@ -1135,10 +806,6 @@ }, "engines": { "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" } }, "node_modules/cross-spawn": { @@ -1229,9 +896,9 @@ } }, "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==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -1241,9 +908,9 @@ } }, "node_modules/esbuild": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", - "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1254,32 +921,32 @@ "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" + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" } }, "node_modules/escape-html": { @@ -1362,13 +1029,12 @@ } }, "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==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", "license": "MIT", "dependencies": { - "debug": "^4.4.3", - "ip-address": "^10.2.0" + "ip-address": "10.0.1" }, "engines": { "node": ">= 16" @@ -1387,9 +1053,9 @@ "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==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", "funding": [ { "type": "github", @@ -1501,6 +1167,19 @@ "node": ">= 0.4" } }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -1526,9 +1205,9 @@ } }, "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==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -1538,9 +1217,9 @@ } }, "node_modules/hono": { - "version": "4.13.7", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.7.tgz", - "integrity": "sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==", + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz", + "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -1589,9 +1268,9 @@ "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==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", "license": "MIT", "engines": { "node": ">= 12" @@ -1770,9 +1449,9 @@ } }, "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==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", "license": "MIT", "funding": { "type": "opencollective", @@ -1802,13 +1481,12 @@ } }, "node_modules/qs": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", - "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", "license": "BSD-3-Clause", "dependencies": { - "es-define-property": "^1.0.1", - "side-channel": "^1.1.1" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -1850,6 +1528,16 @@ "node": ">=0.10.0" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -1945,14 +1633,14 @@ } }, "node_modules/side-channel": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", - "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.4", - "side-channel-list": "^1.0.1", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -1964,13 +1652,13 @@ } }, "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==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" + "object-inspect": "^1.13.3" }, "engines": { "node": ">= 0.4" @@ -2035,13 +1723,14 @@ } }, "node_modules/tsx": { - "version": "4.23.13", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz", - "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.28.0" + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" }, "bin": { "tsx": "dist/cli.mjs" @@ -2054,75 +1743,37 @@ } }, "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==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "license": "MIT", "dependencies": { - "content-type": "^2.0.0", + "content-type": "^1.0.5", "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": ">= 0.6" } }, "node_modules/typescript": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", - "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "bin": { - "tsc": "bin/tsc" + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, "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": ">=14.17" } }, "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==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" }, @@ -2135,6 +1786,19 @@ "node": ">= 0.8" } }, + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -2166,9 +1830,9 @@ "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==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/integration-test/src/jvmTest/typescript/package.json b/integration-test/src/jvmTest/typescript/package.json index b0147e4..6972151 100644 --- a/integration-test/src/jvmTest/typescript/package.json +++ b/integration-test/src/jvmTest/typescript/package.json @@ -4,21 +4,25 @@ "private": true, "scripts": { "client:stdio": "tsx client/stdio-client.ts", - "client:http": "tsx client/http-client.ts http://localhost:3000/mcp greet Hello", + "client:sse": "tsx client/sse-client.ts http://localhost:3000/mcp greet Hello", "server:stdio": "tsx server/stdio-server.ts", - "server:http": "tsx server/http-server.ts" + "server:sse": "tsx server/sse-server.ts" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.30.0", + "@modelcontextprotocol/sdk": "^1.26.0", + "body-parser": "^2.2.2", "express": "^5.2.1", - "zod": "^4.6.2" + "uuid": "^13.0.0", + "zod": "^4.3.5" }, "devDependencies": { + "@eslint/js": "^9.39.2", "@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" + "@types/node": "^24.10.6", + "@types/uuid": "^10.0.0", + "cors": "^2.8.5", + "tsx": "^4.21.0", + "typescript": "^5.9.3" } } diff --git a/integration-test/src/jvmTest/typescript/server/http-server.ts b/integration-test/src/jvmTest/typescript/server/sse-server.ts similarity index 80% rename from integration-test/src/jvmTest/typescript/server/http-server.ts rename to integration-test/src/jvmTest/typescript/server/sse-server.ts index c42e115..48ea453 100644 --- a/integration-test/src/jvmTest/typescript/server/http-server.ts +++ b/integration-test/src/jvmTest/typescript/server/sse-server.ts @@ -1,24 +1,30 @@ +// @ts-ignore import express, {Request, Response} from 'express'; import {randomUUID} from 'node:crypto'; +// @ts-ignore import cors from 'cors'; import {McpServer} from '@modelcontextprotocol/sdk/server/mcp'; -import {StreamableHTTPServerTransport} from '@modelcontextprotocol/sdk/server/streamableHttp'; +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 {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"; +// import {type OAuthMetadata} from '@modelcontextprotocol/sdk/auth.js'; + async function main() { + // 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', @@ -36,15 +42,16 @@ async function main() { const app = express(); app.use(express.json()); - // Allow CORS all domains, expose the Mcp-Session-Id header +// Allow CORS all domains, expose the Mcp-Session-Id header app.use(cors({ - origin: '*', + origin: '*', // Allow all origins exposedHeaders: ["Mcp-Session-Id"] })); - // Set up OAuth if enabled +// 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}`); @@ -72,6 +79,7 @@ async function main() { }).toString() }); + if (!response.ok) { throw new Error(`Invalid or expired token: ${await response.text()}`); } @@ -87,6 +95,7 @@ async function main() { } } + // Convert the response to AuthInfo format return { token, clientId: data.client_id, @@ -95,7 +104,7 @@ async function main() { }; } } - + // Add metadata routes to the main MCP server app.use(mcpAuthMetadataRouter({ oauthMetadata, resourceServerUrl: mcpServerUrl, @@ -110,10 +119,10 @@ async function main() { }); } - // Map to store transports by session ID +// Map to store transports by session ID const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; - // MCP POST endpoint with optional auth +// 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) { @@ -128,18 +137,23 @@ async function main() { 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, + 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]) { @@ -148,12 +162,15 @@ async function main() { } }; + // 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; + return; // Already handled } else { + // Invalid request - no session ID or not initialization request res.status(400).json({ jsonrpc: '2.0', error: { @@ -165,6 +182,8 @@ async function main() { 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); @@ -181,13 +200,14 @@ async function main() { } }; +// Set up routes with conditional auth middleware if (useOAuth && authMiddleware) { app.post('/mcp', authMiddleware, mcpPostHandler); } else { app.post('/mcp', mcpPostHandler); } - // Handle GET requests for event streams +// 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]) { @@ -196,27 +216,29 @@ async function main() { } if (useOAuth && req.auth) { - console.log('Authenticated event stream connection from user:', 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 event stream for session ${sessionId}`); + 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) +// 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]) { @@ -237,6 +259,7 @@ async function main() { } }; +// Set up DELETE route with conditional auth middleware if (useOAuth && authMiddleware) { app.delete('/mcp', authMiddleware, mcpDeleteHandler); } else { @@ -251,10 +274,11 @@ async function main() { console.log(`MCP Streamable HTTP Server listening on port ${MCP_PORT}`); }); - // Handle server shutdown +// 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}`); @@ -267,6 +291,7 @@ async function main() { console.log('Server shutdown complete'); process.exit(0); }); + } main().catch((err) => { diff --git a/integration-test/src/jvmTest/typescript/tsconfig.json b/integration-test/src/jvmTest/typescript/tsconfig.json deleted file mode 100644 index 58e3c67..0000000 --- a/integration-test/src/jvmTest/typescript/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "strict": true, - "target": "ES2022", - "module": "Node16", - "moduleResolution": "Node16", - "esModuleInterop": true, - "skipLibCheck": true, - "noEmit": true - }, - "include": ["**/*.ts"] -} diff --git a/kotlin-sdk-client/api/kotlin-sdk-client.api b/kotlin-sdk-client/api/kotlin-sdk-client.api index d856f5b..268a2b8 100644 --- a/kotlin-sdk-client/api/kotlin-sdk-client.api +++ b/kotlin-sdk-client/api/kotlin-sdk-client.api @@ -34,7 +34,6 @@ public class io/modelcontextprotocol/kotlin/sdk/client/Client : io/modelcontextp 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/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; @@ -42,7 +41,6 @@ public class io/modelcontextprotocol/kotlin/sdk/client/Client : io/modelcontextp 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 { @@ -52,8 +50,8 @@ public final class io/modelcontextprotocol/kotlin/sdk/client/ClientKt { public final class io/modelcontextprotocol/kotlin/sdk/client/ClientOptions : io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptions { public fun ()V - 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 fun (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;Z)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; } @@ -64,18 +62,6 @@ 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/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/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 @@ -102,8 +88,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/client/StdioClientTranspor } 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 final fun getProtocolVersion ()Ljava/lang/String; @@ -122,12 +106,8 @@ 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 fbb1300..f50266c 100644 --- a/kotlin-sdk-client/build.gradle.kts +++ b/kotlin-sdk-client/build.gradle.kts @@ -4,7 +4,6 @@ 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) @@ -15,8 +14,10 @@ kotlin { iosArm64() iosX64() iosSimulatorArm64() + watchosX64() watchosArm64() watchosSimulatorArm64() + tvosX64() tvosArm64() tvosSimulatorArm64() js { @@ -55,6 +56,7 @@ kotlin { implementation(libs.ktor.client.apache5) implementation(libs.mockk) implementation(libs.junit.jupiter.params) + implementation(libs.mokksy) implementation(dependencies.platform(libs.netty.bom)) runtimeOnly(libs.slf4j.simple) } diff --git a/kotlin-sdk-client/detekt-baseline-commonMainSourceSet.xml b/kotlin-sdk-client/detekt-baseline-commonMainSourceSet.xml new file mode 100644 index 0000000..5cddbe1 --- /dev/null +++ b/kotlin-sdk-client/detekt-baseline-commonMainSourceSet.xml @@ -0,0 +1,14 @@ + + + + + CyclomaticComplexMethod:StdioClientTransport.kt:StdioClientTransport$override suspend fun initialize + LongMethod:StdioClientTransport.kt:StdioClientTransport$override suspend fun initialize + MaxLineLength:StdioClientTransport.kt:StdioClientTransport$* + ThrowsCount:SseClientTransport.kt:SseClientTransport$private suspend fun CoroutineScope.collectMessages + ThrowsCount:StreamableHttpClientTransport.kt:StreamableHttpClientTransport$@Suppress("ReturnCount", "CyclomaticComplexMethod") override suspend fun performSend + TooGenericExceptionCaught:Client.kt:Client$error: Throwable + TooGenericExceptionCaught:SseClientTransport.kt:SseClientTransport$e: Throwable + TooGenericExceptionCaught:StdioClientTransport.kt:StdioClientTransport$e: Throwable + + diff --git a/kotlin-sdk-client/detekt-baseline-commonTestSourceSet.xml b/kotlin-sdk-client/detekt-baseline-commonTestSourceSet.xml new file mode 100644 index 0000000..cab4322 --- /dev/null +++ b/kotlin-sdk-client/detekt-baseline-commonTestSourceSet.xml @@ -0,0 +1,8 @@ + + + + + MaxLineLength:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$"""data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}""" + MaxLineLength:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$"""data: {"jsonrpc":"2.0","method":"notifications/resumed","params":{"fromToken":"$lastEventIdSent"}}""" + + diff --git a/kotlin-sdk-client/detekt-baseline-main.xml b/kotlin-sdk-client/detekt-baseline-main.xml index c34e75c..8411ab0 100644 --- a/kotlin-sdk-client/detekt-baseline-main.xml +++ b/kotlin-sdk-client/detekt-baseline-main.xml @@ -2,6 +2,18 @@ - LoopWithTooManyJumpStatements:StreamableHttpClientTransport.kt:StreamableHttpClientTransport$while + CyclomaticComplexMethod:StdioClientTransport.kt:StdioClientTransport$override suspend fun initialize + InjectDispatcher:StdioClientTransport.kt:StdioClientTransport$Default + InjectDispatcher:StreamableHttpClientTransport.kt:StreamableHttpClientTransport$Default + LongMethod:StdioClientTransport.kt:StdioClientTransport$override suspend fun initialize + MaxLineLength:StdioClientTransport.kt:StdioClientTransport$* + NoNameShadowing:StdioClientTransport.kt:StdioClientTransport$source + ThrowsCount:SseClientTransport.kt:SseClientTransport$private suspend fun CoroutineScope.collectMessages + ThrowsCount:StreamableHttpClientTransport.kt:StreamableHttpClientTransport$@Suppress("ReturnCount", "CyclomaticComplexMethod") override suspend fun performSend + TooGenericExceptionCaught:Client.kt:Client$error: Throwable + TooGenericExceptionCaught:SseClientTransport.kt:SseClientTransport$e: Throwable + TooGenericExceptionCaught:StdioClientTransport.kt:StdioClientTransport$e: Throwable + UseCheckOrError:Client.kt:Client$throw IllegalStateException( "Server does not support resource subscriptions (required for $method)", ) + UseCheckOrError:Client.kt:Client$throw IllegalStateException("Client does not support roots capability.") diff --git a/kotlin-sdk-client/detekt-baseline-test.xml b/kotlin-sdk-client/detekt-baseline-test.xml new file mode 100644 index 0000000..b16e8eb --- /dev/null +++ b/kotlin-sdk-client/detekt-baseline-test.xml @@ -0,0 +1,16 @@ + + + + + AbstractClassCanBeConcreteClass:AbstractStreamableHttpClientTest.kt:AbstractStreamableHttpClientTest$AbstractStreamableHttpClientTest + ForbiddenComment:StreamableHttpClientTest.kt:StreamableHttpClientTest$// TODO: how to get notifications via Client API? + InjectDispatcher:StdioClientTransportErrorHandlingTest.kt:StdioClientTransportErrorHandlingTest$IO + InjectDispatcher:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$Default + LongParameterList:MockMcp.kt:MockMcp$fun handleJSONRPCRequest + LongParameterList:MockMcp.kt:MockMcp$fun handleWithResult + LongParameterList:MockMcp.kt:MockMcp$fun onInitialize + MaxLineLength:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$"""data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}""" + MaxLineLength:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$"""data: {"jsonrpc":"2.0","method":"notifications/resumed","params":{"fromToken":"$lastEventIdSent"}}""" + UseOrEmpty:ClientMetaParameterTest.kt:ClientMetaParameterTest$exception.message ?: "" + + 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 3352104..e3added 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 @@ -4,22 +4,16 @@ import io.github.oshai.kotlinlogging.KotlinLogging 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 @@ -28,9 +22,7 @@ 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 @@ -45,26 +37,17 @@ 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 @@ -72,14 +55,8 @@ 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.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 {} @@ -88,17 +65,12 @@ private val logger = KotlinLogging.logger {} * Options for configuring the MCP client. * * @property capabilities The capabilities this client supports. - * @param enforceStrictCapabilities Whether to strictly enforce capabilities when interacting with the server. - * @param handlerCoroutineContext Coroutine context for inbound handlers. See [ProtocolOptions.handlerCoroutineContext]. + * @property enforceStrictCapabilities Whether to strictly enforce capabilities when interacting with the server. */ public class ClientOptions( public val capabilities: ClientCapabilities = ClientCapabilities(), enforceStrictCapabilities: Boolean = true, - handlerCoroutineContext: CoroutineContext = Dispatchers.Default, -) : ProtocolOptions( - enforceStrictCapabilities = enforceStrictCapabilities, - handlerCoroutineContext = handlerCoroutineContext, -) +) : ProtocolOptions(enforceStrictCapabilities = enforceStrictCapabilities) /** * Initializes and connects an MCP client using the provided clientInfo [Implementation], client options, @@ -135,6 +107,7 @@ public suspend fun mcpClient( * @param clientInfo Information about the client implementation (name, version). * @param options Configuration options for this client. */ +@Suppress("TooManyFunctions") public open class Client(private val clientInfo: Implementation, options: ClientOptions = ClientOptions()) : Protocol(options) { @@ -184,7 +157,6 @@ 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 } @@ -223,7 +195,6 @@ public open class Client(private val clientInfo: Implementation, options: Client serverInstructions = result.instructions notification(InitializedNotification()) - enableConcurrentDispatch() } catch (error: Throwable) { logger.error(error) { "Failed to initialize client: ${error.message}" } close() @@ -250,15 +221,10 @@ public open class Client(private val clientInfo: Implementation, options: Client Method.Defined.PromptsGet, Method.Defined.PromptsList, + Method.Defined.CompletionComplete, -> { 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)" + throw IllegalStateException("Server does not support prompts (required for $method)") } } @@ -271,10 +237,10 @@ 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) { - check(resCaps.subscribe == true) { - "Server does not support resource subscriptions (required for $method)" - } + if (method == Method.Defined.ResourcesSubscribe && resCaps.subscribe != true) { + throw IllegalStateException( + "Server does not support resource subscriptions (required for $method)", + ) } } @@ -284,12 +250,6 @@ public open class Client(private val clientInfo: Implementation, options: Client } } - Method.Defined.TasksGet, - Method.Defined.TasksResult, - Method.Defined.TasksList, - Method.Defined.TasksCancel, - -> assertTasksCapabilityForMethod(method) - Method.Defined.Initialize, Method.Defined.Ping -> { // No specific capability required } @@ -300,24 +260,6 @@ 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 -> { @@ -326,12 +268,6 @@ public open class Client(private val clientInfo: Implementation, options: Client } } - Method.Defined.NotificationsTasksStatus -> { - checkNotNull(capabilities.tasks) { - "Client does not support tasks (required for $method)" - } - } - Method.Defined.NotificationsInitialized, Method.Defined.NotificationsCancelled, Method.Defined.NotificationsProgress, @@ -373,26 +309,6 @@ 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. * @@ -407,7 +323,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 completions. + * @throws IllegalStateException If the server does not support prompts or completion. */ public suspend fun complete(params: CompleteRequest, options: RequestOptions? = null): CompleteResult = request(params, options) @@ -602,8 +518,9 @@ 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 { - checkNotNull(capabilities.roots) { - "Client does not support roots capability." + 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.") } logger.info { "Removing root: $uri" } val oldMap = roots.getAndUpdate { current -> current.remove(uri) } @@ -659,19 +576,6 @@ 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. */ @@ -683,88 +587,12 @@ public open class Client(private val clientInfo: Implementation, options: Client logger.info { "Setting the elicitation handler" } 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) + handler(request) } } // --- Internal Handlers --- - 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) 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 deleted file mode 100644 index 95c5bde..0000000 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions.kt +++ /dev/null @@ -1,44 +0,0 @@ -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 deleted file mode 100644 index 1290cb0..0000000 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SamplingValidation.kt +++ /dev/null @@ -1,24 +0,0 @@ -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 index 0295eb9..3c9d88b 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 @@ -24,12 +24,11 @@ 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.ExperimentalAtomicApi import kotlin.time.Duration @@ -136,7 +135,6 @@ public class SseClientTransport( throw e } finally { closeResources() - invokeOnCloseCallback() } } @@ -173,15 +171,13 @@ public class SseClientTransport( } 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) - } + job?.cancelAndJoin() + try { + if (::session.isInitialized) session.cancel() + if (::scope.isInitialized) scope.cancel() + endpoint.cancel() + } catch (e: Throwable) { + _onError(e) } } } 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 5e990ee..12529cc 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 @@ -21,7 +21,6 @@ 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 @@ -32,7 +31,6 @@ 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 @@ -227,6 +225,7 @@ public class StdioClientTransport @JvmOverloads public constructor( } override suspend fun performSend(message: JSONRPCMessage, options: TransportSendOptions?) { + @Suppress("SwallowedException") try { sendChannel.send(message) } catch (e: CancellationException) { @@ -242,20 +241,8 @@ public class StdioClientTransport @JvmOverloads public constructor( } 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" } } - } + scope.stopProcessing("Closed") + scope.coroutineContext[Job]?.join() // Wait for all coroutines to complete } private fun sendOutboundMessage(message: JSONRPCMessage, sink: Sink, mainScope: CoroutineScope) { @@ -277,8 +264,6 @@ public class StdioClientTransport @JvmOverloads public constructor( 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) } @@ -299,7 +284,7 @@ public class StdioClientTransport @JvmOverloads public constructor( ) { val buffer = Buffer() try { - source.use { + source.use { source -> while (isActive) { val bytesRead = source.readAtMostTo(buffer, BUFFER_SIZE) if (bytesRead == -1L) { 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 6df352a..a3b4190 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 @@ -21,10 +21,8 @@ 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.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 @@ -40,117 +38,45 @@ 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 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.concurrent.atomics.ExperimentalAtomicApi import kotlin.time.Duration -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 = "?=" /** - * 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 + * Error class for Streamable HTTP transport errors. */ 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 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 + * 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. */ +@OptIn(ExperimentalAtomicApi::class) public class StreamableHttpClientTransport( private val client: HttpClient, private val url: String, - private val reconnectionOptions: ReconnectionOptions = ReconnectionOptions(), - private val maxInlineSseEventSize: Int = DEFAULT_MAX_INLINE_SSE_EVENT_SIZE, + private val reconnectionTime: Duration? = null, private val requestBuilder: HttpRequestBuilder.() -> Unit = {}, ) : 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 var sseSession: ClientSSESession? = null private var sseJob: Job? = null private val scope by lazy { CoroutineScope(SupervisorJob() + Dispatchers.Default) } - /** 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, - ) + private var lastEventId: String? = null override suspend fun initialize() { logger.debug { "Client transport is starting..." } @@ -159,6 +85,7 @@ public class StreamableHttpClientTransport( /** * Sends a single message with optional resumption support */ + @Suppress("ReturnCount", "CyclomaticComplexMethod") override suspend fun performSend(message: JSONRPCMessage, options: TransportSendOptions?) { logger.debug { "Client sending message via POST to $url: ${McpJson.encodeToString(message)}" } @@ -175,7 +102,6 @@ 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) @@ -207,25 +133,18 @@ public class StreamableHttpClientTransport( } } - 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, - ) - } - } + ContentType.Text.EventStream -> handleInlineSse( + response, + onResumptionToken = options?.onResumptionToken, + replayMessageId = if (message is JSONRPCRequest) message.id else null, + ) 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 } @@ -250,6 +169,11 @@ public class StreamableHttpClientTransport( override suspend fun closeResources() { logger.debug { "Client transport closing." } + + // Try to terminate session if we have one + terminateSession() + + sseSession?.cancel() sseJob?.cancelAndJoin() scope.cancel() } @@ -277,118 +201,55 @@ public class StreamableHttpClientTransport( } sessionId = null + lastEventId = null logger.debug { "Session terminated successfully" } } - private fun startSseSession( + private suspend fun startSseSession( resumptionToken: String? = null, replayMessageId: RequestId? = null, onResumptionToken: ((String) -> Unit)? = null, - initialServerRetryDelay: Duration? = null, ) { - // 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 + sseSession?.cancel() + sseJob?.cancelAndJoin() - 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" } - return try { - val session = client.sseSession(urlString = url, showRetryEvents = true) { + try { + sseSession = client.sseSession( + urlString = url, + reconnectionTime = reconnectionTime, + ) { method = HttpMethod.Get applyCommonHeaders(this) + // sseSession will add ContentType.Text.EventStream automatically accept(ContentType.Application.Json) - lastEventId?.let { headers.append(MCP_RESUMPTION_TOKEN_HEADER, it) } + (resumptionToken ?: lastEventId)?.let { headers.append(MCP_RESUMPTION_TOKEN_HEADER, it) } requestBuilder() } logger.debug { "Client SSE session started successfully." } - 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 - } - } + val responseStatus = e.response?.status + val responseContentType = e.response?.contentType() - 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 + // 405 means server doesn't support SSE at GET endpoint - this is expected and valid + if (responseStatus == HttpStatusCode.MethodNotAllowed) { + logger.info { "Server returned 405 for GET/SSE, stream disabled." } + return } - responseContentType?.match(ContentType.Application.Json) == true -> { + // If server returns application/json, it means it doesn't support SSE for this session + // This is valid per spec - server can choose to only use JSON responses + if (responseContentType?.match(ContentType.Application.Json) == true) { logger.info { "Server returned application/json for GET/SSE, using JSON-only mode." } - true + return } - else -> false + _onError(e) + throw e + } + + sseJob = scope.launch(CoroutineName("StreamableHttpTransport.collect#${hashCode()}")) { + sseSession?.let { collectSse(it, replayMessageId, onResumptionToken) } } } @@ -399,51 +260,16 @@ 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" - } - + @Suppress("TooGenericExceptionCaught") 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 { - localLastEventId = it - hasPrimingEvent = true + lastEventId = it onResumptionToken?.invoke(it) } logger.trace { "Client received SSE event: event=${event.event}, data=${event.data}, id=${event.id}" } @@ -452,7 +278,6 @@ 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 { @@ -470,29 +295,24 @@ public class StreamableHttpClientTransport( } catch (t: Throwable) { _onError(t) } - return SseStreamResult(hasPrimingEvent, receivedResponse, localLastEventId, localServerRetryDelay) } + @Suppress("CyclomaticComplexMethod") 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(id: String?, eventName: String?, data: String) { id?.let { - localLastEventId = it - hasPrimingEvent = true + lastEventId = it onResumptionToken?.invoke(it) } if (data.isBlank()) { @@ -501,7 +321,6 @@ public class StreamableHttpClientTransport( 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 { @@ -519,15 +338,9 @@ public class StreamableHttpClientTransport( } } + @Suppress("LoopWithTooManyJumpStatements") while (!channel.isClosedForRead) { - // 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 + val line = channel.readUTF8Line() ?: break if (line.isEmpty()) { dispatch(id = id, eventName = eventName, data = sb.toString()) // reset @@ -538,22 +351,9 @@ public class StreamableHttpClientTransport( } 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()) - // 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 - } + line.startsWith("data:") -> sb.append(line.substringAfter("data:").trim()) } } - 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 a618f18..b64a220 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 @@ -6,67 +6,21 @@ 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 reconnectionTime Optional duration to wait before attempting to reconnect. * @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(), + reconnectionTime: Duration? = null, 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. - * - * @param url 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 [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?, - requestBuilder: HttpRequestBuilder.() -> Unit = {}, -): 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 -} + StreamableHttpClientTransport(this, url, reconnectionTime, requestBuilder = requestBuilder) /** * Creates and connects an MCP client over Streamable HTTP using the provided HttpClient. @@ -76,23 +30,12 @@ public suspend fun HttpClient.mcpStreamableHttp( * @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?, + reconnectionTime: Duration? = null, requestBuilder: HttpRequestBuilder.() -> Unit = {}, ): Client { - val transport = mcpStreamableHttpTransport( - url, - ReconnectionOptions(initialReconnectionDelay = reconnectionTime ?: 1.seconds), - requestBuilder, - ) + val transport = mcpStreamableHttpTransport(url, reconnectionTime, requestBuilder) val client = Client(Implementation(name = IMPLEMENTATION_NAME, version = LIB_VERSION)) client.connect(transport) return client 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 deleted file mode 100644 index e9379a7..0000000 --- a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientAssertCapabilityTest.kt +++ /dev/null @@ -1,171 +0,0 @@ -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/sse/MockSseClientEngine.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/MockSseClientEngine.kt index 14af546..3627b6c 100644 --- 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 @@ -53,14 +53,6 @@ internal open class MockSseClientEngine( 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 { 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 index 319ba2a..6cc7f6e 100644 --- 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 @@ -1,6 +1,5 @@ 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 @@ -10,7 +9,6 @@ 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 { @@ -92,31 +90,6 @@ class SseClientTransportTest { 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) 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 index d9aaf7a..e303326 100644 --- 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 @@ -19,16 +19,13 @@ 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 @@ -44,15 +41,11 @@ 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 { + private fun createTransport(handler: MockRequestHandler): StreamableHttpClientTransport { val mockEngine = MockEngine(handler) val httpClient = HttpClient(mockEngine) { install(SSE) { @@ -60,11 +53,7 @@ class StreamableHttpClientTransportTest { } } - return StreamableHttpClientTransport( - httpClient, - url = "http://localhost:8080/mcp", - maxInlineSseEventSize = maxInlineSseEventSize, - ) + return StreamableHttpClientTransport(httpClient, url = "http://localhost:8080/mcp") } private fun buildSseMessage(id: String, method: String, params: String): String = buildString { @@ -157,17 +146,18 @@ class StreamableHttpClientTransportTest { ) val transport = createTransport { request -> - val msg = McpJson.decodeFromString( - (request.body as TextContent).text, - ) - when { - msg is JSONRPCRequest && msg.method == "initialize" -> respond( + 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"), ) - msg is JSONRPCNotification && msg.method == "test" -> { + is JSONRPCNotification if msg.method == "test" -> { assertEquals("test-session-id", request.headers["mcp-session-id"]) respond( content = "", @@ -246,100 +236,6 @@ class StreamableHttpClientTransportTest { 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 @@ -348,8 +244,8 @@ class StreamableHttpClientTransportTest { var sseStarted = false val transport = createTransport { request -> - when { - request.method == HttpMethod.Post && request.body.toString().contains("notifications/initialized") -> { + when (request.method) { + HttpMethod.Post if request.body.toString().contains("notifications/initialized") -> { respond( content = "", status = HttpStatusCode.Accepted, @@ -358,7 +254,7 @@ class StreamableHttpClientTransportTest { } // Handle SSE connection - request.method == HttpMethod.Get -> { + HttpMethod.Get -> { sseStarted = true val sseContent = buildString { // Server sends various notifications @@ -390,7 +286,7 @@ class StreamableHttpClientTransportTest { } // Handle regular notifications - request.method == HttpMethod.Post -> { + HttpMethod.Post -> { respond( content = "", status = HttpStatusCode.Accepted, @@ -581,170 +477,6 @@ class StreamableHttpClientTransportTest { } } - @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 -> @@ -910,7 +642,7 @@ class StreamableHttpClientTransportTest { eventually { while (receivedMessages.isEmpty()) { - delay(10.milliseconds) + delay(10) } } @@ -925,202 +657,6 @@ class StreamableHttpClientTransportTest { 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> { diff --git a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/AbstractStreamableHttpClientTest.kt b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/AbstractStreamableHttpClientTest.kt new file mode 100644 index 0000000..4a04e81 --- /dev/null +++ b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/AbstractStreamableHttpClientTest.kt @@ -0,0 +1,42 @@ +package io.modelcontextprotocol.kotlin.sdk.client + +import io.ktor.client.HttpClient +import io.ktor.client.engine.apache5.Apache5 +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logging +import io.ktor.client.plugins.sse.SSE +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal abstract class AbstractStreamableHttpClientTest { + + // start mokksy on random port + protected val mockMcp: MockMcp = MockMcp(verbose = true) + + @AfterEach + fun afterEach() { + mockMcp.verifyNoUnexpectedRequests() + } + + @AfterAll + fun afterAll() { + mockMcp.verifyNoUnmatchedStubs() + mockMcp.close() + } + + protected suspend fun connect(client: Client) { + client.connect( + StreamableHttpClientTransport( + url = mockMcp.url, + client = HttpClient(Apache5) { + install(SSE) + install(Logging) { + level = LogLevel.ALL + } + }, + ), + ) + } +} 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 deleted file mode 100644 index 7775a08..0000000 --- a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientSamplingValidationTest.kt +++ /dev/null @@ -1,98 +0,0 @@ -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/MockMcp.kt b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockMcp.kt new file mode 100644 index 0000000..fb71904 --- /dev/null +++ b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockMcp.kt @@ -0,0 +1,241 @@ +package io.modelcontextprotocol.kotlin.sdk.client + +import dev.mokksy.mokksy.BuildingStep +import dev.mokksy.mokksy.Mokksy +import dev.mokksy.mokksy.StubConfiguration +import dev.mokksy.mokksy.shutdown +import dev.mokksy.mokksy.start +import io.ktor.http.ContentType +import io.ktor.http.HttpMethod +import io.ktor.http.HttpStatusCode +import io.ktor.sse.ServerSentEvent +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest +import io.modelcontextprotocol.kotlin.sdk.types.RequestId +import kotlinx.coroutines.flow.Flow +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.putJsonObject + +const val MCP_SESSION_ID_HEADER = "MCP-Session-Id" + +internal class MockMcp(verbose: Boolean = false) : AutoCloseable { + + private val mokksy: Mokksy = Mokksy(verbose = verbose).apply { + start() + } + + fun verifyNoUnexpectedRequests() { + mokksy.verifyNoUnexpectedRequests() + } + + fun verifyNoUnmatchedStubs() { + mokksy.verifyNoUnmatchedStubs() + } + + val url = "${mokksy.baseUrl()}/mcp" + + @Suppress("LongParameterList") + fun onInitialize( + clientName: String? = null, + sessionId: String, + protocolVersion: String = "2025-03-26", + serverName: String = "Mock MCP Server", + serverVersion: String = "1.0.0", + capabilities: JsonObject = buildJsonObject { + putJsonObject("tools") { + put("listChanged", JsonPrimitive(false)) + } + }, + ) { + val predicates = if (clientName != null) { + arrayOf<(JSONRPCRequest?) -> Boolean>({ + it?.params?.jsonObject + ?.get("clientInfo")?.jsonObject + ?.get("name")?.jsonPrimitive + ?.contentOrNull == clientName + }) + } else { + emptyArray() + } + + handleWithResult( + jsonRpcMethod = "initialize", + sessionId = sessionId, + bodyPredicates = predicates, + // language=json + result = """ + { + "capabilities": $capabilities, + "protocolVersion": "$protocolVersion", + "serverInfo": { + "name": "$serverName", + "version": "$serverVersion" + }, + "_meta": { + "foo": "bar" + } + } + """.trimIndent(), + ) + } + + fun onJSONRPCRequest( + httpMethod: HttpMethod = HttpMethod.Post, + jsonRpcMethod: String, + expectedSessionId: String? = null, + vararg bodyPredicates: (JSONRPCRequest) -> Boolean, + ): BuildingStep = mokksy.method( + configuration = StubConfiguration(removeAfterMatch = true), + httpMethod = httpMethod, + requestType = JSONRPCRequest::class, + ) { + path("/mcp") + expectedSessionId?.let { + containsHeader(MCP_SESSION_ID_HEADER, it) + } + bodyMatchesPredicate( + description = "JSON-RPC version is '2.0'", + predicate = + { + it!!.jsonrpc == "2.0" + }, + ) + bodyMatchesPredicate( + description = "JSON-RPC Method should be '$jsonRpcMethod'", + predicate = + { + it!!.method == jsonRpcMethod + }, + ) + bodyPredicates.forEach { predicate -> + bodyMatchesPredicate(predicate = { predicate.invoke(it!!) }) + } + } + + @Suppress("LongParameterList") + fun handleWithResult( + httpMethod: HttpMethod = HttpMethod.Post, + jsonRpcMethod: String, + expectedSessionId: String? = null, + sessionId: String, + contentType: ContentType = ContentType.Application.Json, + statusCode: HttpStatusCode = HttpStatusCode.OK, + vararg bodyPredicates: (JSONRPCRequest) -> Boolean, + result: () -> JsonObject, + ) { + onJSONRPCRequest( + httpMethod = httpMethod, + jsonRpcMethod = jsonRpcMethod, + expectedSessionId = expectedSessionId, + bodyPredicates = bodyPredicates, + ) respondsWith { + val requestId = when (val id = request.body().id) { + is RequestId.NumberId -> id.value.toString() + is RequestId.StringId -> "\"${id.value}\"" + } + val resultObject = result.invoke() + // language=json + body = """ + { + "jsonrpc": "2.0", + "id": $requestId, + "result": $resultObject + } + """.trimIndent() + this.contentType = contentType + headers += MCP_SESSION_ID_HEADER to sessionId + httpStatus = statusCode + } + } + + @Suppress("LongParameterList") + fun handleWithResult( + httpMethod: HttpMethod = HttpMethod.Post, + jsonRpcMethod: String, + expectedSessionId: String? = null, + sessionId: String, + contentType: ContentType = ContentType.Application.Json, + statusCode: HttpStatusCode = HttpStatusCode.OK, + vararg bodyPredicates: (JSONRPCRequest) -> Boolean, + result: String, + ) { + handleWithResult( + httpMethod = httpMethod, + jsonRpcMethod = jsonRpcMethod, + expectedSessionId = expectedSessionId, + sessionId = sessionId, + contentType = contentType, + statusCode = statusCode, + bodyPredicates = bodyPredicates, + result = { + Json.parseToJsonElement(result).jsonObject + }, + ) + } + + @Suppress("LongParameterList") + fun handleJSONRPCRequest( + httpMethod: HttpMethod = HttpMethod.Post, + jsonRpcMethod: String, + expectedSessionId: String? = null, + sessionId: String, + contentType: ContentType = ContentType.Application.Json, + statusCode: HttpStatusCode = HttpStatusCode.OK, + vararg bodyPredicates: (JSONRPCRequest?) -> Boolean, + bodyBuilder: () -> String = { "" }, + ) { + onJSONRPCRequest( + httpMethod = httpMethod, + jsonRpcMethod = jsonRpcMethod, + expectedSessionId = expectedSessionId, + bodyPredicates = bodyPredicates, + ) respondsWith { + body = bodyBuilder.invoke() + this.contentType = contentType + headers += MCP_SESSION_ID_HEADER to sessionId + httpStatus = statusCode + } + } + + fun onSubscribe(httpMethod: HttpMethod = HttpMethod.Post, sessionId: String): BuildingStep = mokksy.method( + httpMethod = httpMethod, + name = "MCP GETs", + requestType = Any::class, + ) { + path("/mcp") + containsHeader(MCP_SESSION_ID_HEADER, sessionId) + containsHeader("Accept", "application/json,text/event-stream") + containsHeader("Cache-Control", "no-store") + } + + fun handleSubscribeWithGet(sessionId: String, block: () -> Flow) { + onSubscribe( + httpMethod = HttpMethod.Get, + sessionId = sessionId, + ) respondsWithSseStream { + headers += MCP_SESSION_ID_HEADER to sessionId + this.flow = block.invoke() + } + } + + fun mockUnsubscribeRequest(sessionId: String) { + mokksy.delete( + configuration = StubConfiguration(removeAfterMatch = true), + requestType = JSONRPCRequest::class, + ) { + path("/mcp") + containsHeader(MCP_SESSION_ID_HEADER, sessionId) + } respondsWith { + body = null + } + } + + override fun close() { + mokksy.shutdown() + } +} diff --git a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTest.kt b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTest.kt new file mode 100644 index 0000000..6513b46 --- /dev/null +++ b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTest.kt @@ -0,0 +1,270 @@ +package io.modelcontextprotocol.kotlin.sdk.client + +import io.kotest.matchers.collections.shouldContain +import io.ktor.http.ContentType +import io.ktor.http.HttpMethod +import io.ktor.http.HttpStatusCode +import io.ktor.sse.ServerSentEvent +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 kotlinx.coroutines.delay +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonObject +import org.junit.jupiter.api.TestInstance +import kotlin.test.Test +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +@OptIn(ExperimentalUuidApi::class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class StreamableHttpClientTest : AbstractStreamableHttpClientTest() { + + @Test + fun `Should skip empty SSE`(): Unit = runBlocking { + val client = Client( + clientInfo = Implementation( + name = "client1", + version = "1.0.0", + ), + options = ClientOptions( + capabilities = ClientCapabilities(), + ), + ) + val sessionId = Uuid.random().toString() + + mockMcp.onJSONRPCRequest( + httpMethod = HttpMethod.Post, + jsonRpcMethod = "initialize", + ).respondsWithStream { + headers += MCP_SESSION_ID_HEADER to sessionId + flow = flowOf( + "id: ${Uuid.random()}\n", + "data:\n", // empty data + "\n", + "id: ${Uuid.random()}\n", + "data: \t \n", // tabs and spaces + "\n", + "id: ${Uuid.random()}\n", + "event: message\n", + // multiline data + "data: {\n", + "data: \"result\":{\n" + + "data: \"protocolVersion\":\"2025-06-18\",\n" + + "data: \"capabilities\":{},\n" + + "data: \"serverInfo\":{\"name\":\"simple-streamable-http-server\",\"version\":\"1.0.0\"}\n" + + "data: },\n" + + "data: \"jsonrpc\":\"2.0\",\n" + + "data: \"id\":\"7ce065b0678f49e5b04ce5a0fcc7d518\"\n" + + "data: }\n", + "\n", + ) + } + + mockMcp.handleJSONRPCRequest( + jsonRpcMethod = "notifications/initialized", + expectedSessionId = sessionId, + sessionId = sessionId, + statusCode = HttpStatusCode.Accepted, + ) + + mockMcp.handleSubscribeWithGet(sessionId) { + emptyFlow() + } + + connect(client) + } + + @Test + fun `test streamableHttpClient`() = runBlocking { + val client = Client( + clientInfo = Implementation( + name = "client1", + version = "1.0.0", + ), + options = ClientOptions( + capabilities = ClientCapabilities(), + ), + ) + + val sessionId = Uuid.random().toString() + + mockMcp.onInitialize( + clientName = "client1", + sessionId = sessionId, + ) + + mockMcp.handleJSONRPCRequest( + jsonRpcMethod = "notifications/initialized", + expectedSessionId = sessionId, + sessionId = sessionId, + statusCode = HttpStatusCode.Accepted, + ) + + mockMcp.handleSubscribeWithGet(sessionId) { + flow { + delay(500.milliseconds) + emit( + ServerSentEvent( + event = "message", + id = "1", + data = @Suppress("MaxLineLength") + //language=json + """{"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}""", + ), + ) + delay(200.milliseconds) + emit( + ServerSentEvent( + data = @Suppress("MaxLineLength") + //language=json + """{"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}""", + ), + ) + } + } + + // TODO: how to get notifications via Client API? + + mockMcp.handleWithResult( + jsonRpcMethod = "tools/list", + sessionId = sessionId, + // language=json + 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(), + ) + + connect(client) + + 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, + ) + + mockMcp.mockUnsubscribeRequest(sessionId = sessionId) + + client.close() + } + + @Test + fun `handle MethodNotAllowed`() = runBlocking { + checkSupportNonStreamingResponse( + ContentType.Text.EventStream, + HttpStatusCode.MethodNotAllowed, + ) + } + + @Test + fun `handle non-streaming response`() = runBlocking { + checkSupportNonStreamingResponse( + ContentType.Application.Json, + HttpStatusCode.OK, + ) + } + + private suspend fun checkSupportNonStreamingResponse(contentType: ContentType, statusCode: HttpStatusCode) { + val sessionId = "SID_${Uuid.random().toHexString()}" + val clientName = "client-${Uuid.random().toHexString()}" + val client = Client( + clientInfo = Implementation(name = clientName, version = "1.0.0"), + options = ClientOptions( + capabilities = ClientCapabilities(), + ), + ) + + mockMcp.onInitialize(clientName = clientName, sessionId = sessionId) + + mockMcp.handleJSONRPCRequest( + jsonRpcMethod = "notifications/initialized", + expectedSessionId = sessionId, + sessionId = sessionId, + statusCode = HttpStatusCode.Accepted, + ) + + mockMcp.onSubscribe( + httpMethod = HttpMethod.Get, + sessionId = sessionId, + ) respondsWith { + headers += MCP_SESSION_ID_HEADER to sessionId + body = null + httpStatus = statusCode + this.contentType = contentType + } + + mockMcp.handleWithResult(jsonRpcMethod = "ping", sessionId = sessionId) { + buildJsonObject {} + } + + mockMcp.mockUnsubscribeRequest(sessionId = sessionId) + + connect(client) + + delay(1.seconds) + + client.ping() // connection is still alive + + client.close() + } +} diff --git a/kotlin-sdk-core/.openapi-generator-ignore b/kotlin-sdk-core/.openapi-generator-ignore new file mode 100644 index 0000000..dbdf930 --- /dev/null +++ b/kotlin-sdk-core/.openapi-generator-ignore @@ -0,0 +1,29 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +**/auth/** +**/infrastructure/** +!**/Base64ByteArray.kt +!**/Bytes.kt + + +## Exclude Models which are causing problems +**/JSONRPCResponse.kt +**/Result.kt +**/ListToolsResult.kt +**/InitializeRequest.kt +**/InitializeResult.kt +**/ServerCapabilities.kt +**/InitializeRequestParams.kt +**/CreateMessageRequest.kt +**/CreateMessageRequestParams.kt +**/ClientCapabilities.kt +**/Tool.kt +**/ToolInputSchema.kt +**/ToolOutputSchema.kt + diff --git a/kotlin-sdk-core/api/kotlin-sdk-core.api b/kotlin-sdk-core/api/kotlin-sdk-core.api index b3e88d8..0607df5 100644 --- a/kotlin-sdk-core/api/kotlin-sdk-core.api +++ b/kotlin-sdk-core/api/kotlin-sdk-core.api @@ -1,6 +1,144 @@ +public final class io/modelcontextprotocol/kotlin/sdk/CallToolResult { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CallToolResult; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/CallToolResultBase { + 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 { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification; + public final fun Params (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotificationParams; + public static synthetic fun Params$default (Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification;Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotificationParams; +} + +public final class io/modelcontextprotocol/kotlin/sdk/ClientCapabilities { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities; + public final fun Roots (Ljava/lang/Boolean;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots; + public static synthetic fun Roots$default (Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities;Ljava/lang/Boolean;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots; +} + +public final class io/modelcontextprotocol/kotlin/sdk/CompatibilityCallToolResult { + 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 final fun getContent ()Ljava/util/List; + public final fun getStructuredContent ()Lkotlinx/serialization/json/JsonObject; + public final fun getToolResult ()Lkotlinx/serialization/json/JsonObject; + public final fun get_meta ()Lkotlinx/serialization/json/JsonObject; + public final 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 { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest; + public final fun Argument (Ljava/lang/String;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Argument; +} + +public final class io/modelcontextprotocol/kotlin/sdk/CompleteResult { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CompleteResult; + public final fun Completion (Ljava/util/List;Ljava/lang/Integer;Ljava/lang/Boolean;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult$Completion; + public static synthetic fun Completion$default (Lio/modelcontextprotocol/kotlin/sdk/CompleteResult;Ljava/util/List;Ljava/lang/Integer;Ljava/lang/Boolean;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult$Completion; +} + +public final class io/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest; + public final fun RequestedSchema (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema; + public static synthetic fun RequestedSchema$default (Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema; +} + +public final class io/modelcontextprotocol/kotlin/sdk/CreateElicitationResult { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationResult; +} + +public final class io/modelcontextprotocol/kotlin/sdk/CreateElicitationResult$Action { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationResult$Action; + public final fun getAccept ()Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult$Action; + public final fun getCancel ()Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult$Action; + public final fun getDecline ()Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult$Action; +} + +public final class io/modelcontextprotocol/kotlin/sdk/CreateMessageRequest { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest; + public final fun getIncludeContext ()Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext$Companion; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/ErrorCode { + public abstract fun getCode ()I +} + +public final class io/modelcontextprotocol/kotlin/sdk/ErrorCode$Defined { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ErrorCode$Defined; + public final fun getConnectionClosed ()I + public final fun getInternalError ()I + public final fun getInvalidParams ()I + public final fun getInvalidRequest ()I + public final fun getMethodNotFound ()I + public final fun getParseError ()I + public final fun getRequestTimeout ()I +} + +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 abstract interface annotation class io/modelcontextprotocol/kotlin/sdk/ExperimentalMcpApi : java/lang/annotation/Annotation { } +public final class io/modelcontextprotocol/kotlin/sdk/InitializedNotification { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification; + public final fun Params (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; + public static synthetic fun Params$default (Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; +} + public abstract interface annotation class io/modelcontextprotocol/kotlin/sdk/InternalMcpApi : java/lang/annotation/Annotation { } @@ -8,6 +146,267 @@ public final class io/modelcontextprotocol/kotlin/sdk/LibVersionKt { public static final field LIB_VERSION Ljava/lang/String; } +public final class io/modelcontextprotocol/kotlin/sdk/LoggingLevel { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel; + public final fun getAlert ()Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; + public final fun getCritical ()Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; + public final fun getDebug ()Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; + public final fun getEmergency ()Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; + public final fun getError ()Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; + public final fun getInfo ()Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; + public final fun getNotice ()Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; + public final fun getWarning ()Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; +} + +public final class io/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification; + public final fun Params (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 Params$default (Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification;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 final fun SetLevelRequest (Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequest; +} + +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 Custom (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/Method$Custom; +} + +public final class io/modelcontextprotocol/kotlin/sdk/Method$Defined { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/Method$Defined; + public final fun getCompletionComplete ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public final fun getElicitationCreate ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public final fun getInitialize ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public final fun getLoggingSetLevel ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public final fun getNotificationsCancelled ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public final fun getNotificationsInitialized ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public final fun getNotificationsMessage ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public final fun getNotificationsProgress ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public final fun getNotificationsPromptsListChanged ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public final fun getNotificationsResourcesListChanged ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public final fun getNotificationsResourcesUpdated ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public final fun getNotificationsRootsListChanged ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public final fun getNotificationsToolsListChanged ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public final fun getPing ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public final fun getPromptsGet ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public final fun getPromptsList ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public final fun getResourcesList ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public final fun getResourcesRead ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public final fun getResourcesSubscribe ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public final fun getResourcesTemplatesList ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public final fun getResourcesUnsubscribe ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public final fun getRootsList ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public final fun getSamplingCreateMessage ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public final fun getToolsCall ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public final fun getToolsList ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; +} + +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 { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification; + public final fun Params (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 Params$default (Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification;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 final class io/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification; + public final fun Params (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; + public static synthetic fun Params$default (Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; +} + +public final class io/modelcontextprotocol/kotlin/sdk/RequestId { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/RequestId; + public final fun NumberId-Nt6Vbyc (J)J + public final fun StringId-QNUQp7g (Ljava/lang/String;)Ljava/lang/String; +} + +public final class io/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification; + public final fun Params (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; + public static synthetic fun Params$default (Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; +} + +public final class io/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification; + public final fun Params (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotificationParams; + public static synthetic fun Params$default (Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotificationParams; +} + +public final class io/modelcontextprotocol/kotlin/sdk/Role { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/Role; + public final fun getAssistant ()Lio/modelcontextprotocol/kotlin/sdk/types/Role; + public final fun getUser ()Lio/modelcontextprotocol/kotlin/sdk/types/Role; +} + +public final class io/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification; + public final fun Params (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; + public static synthetic fun Params$default (Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; +} + +public final class io/modelcontextprotocol/kotlin/sdk/ServerCapabilities { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities; + public final fun Prompts (Ljava/lang/Boolean;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts; + public static synthetic fun Prompts$default (Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;Ljava/lang/Boolean;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts; + public final fun Resources (Ljava/lang/Boolean;Ljava/lang/Boolean;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources; + public static synthetic fun Resources$default (Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;Ljava/lang/Boolean;Ljava/lang/Boolean;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources; + public final fun Tools (Ljava/lang/Boolean;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools; + public static synthetic fun Tools$default (Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;Ljava/lang/Boolean;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools; +} + +public final class io/modelcontextprotocol/kotlin/sdk/Tool { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/Tool; + public final fun Input (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema; + public static synthetic fun Input$default (Lio/modelcontextprotocol/kotlin/sdk/Tool;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema; + public final fun Output (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema; + public static synthetic fun Output$default (Lio/modelcontextprotocol/kotlin/sdk/Tool;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema; +} + +public final class io/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification; + public final fun Params (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; + public static synthetic fun Params$default (Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; +} + +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 CallToolResult (Ljava/util/List;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult; + public static synthetic fun CallToolResult$default (Ljava/util/List;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult; + public static final fun CancelledNotification (Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotification; + public static final fun ClientCapabilities (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; + public static synthetic fun ClientCapabilities$default (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; + public static final fun CompleteRequest (Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequest; + public static final fun CompleteResult (Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult$Completion;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult; + public static synthetic fun CompleteResult$default (Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult$Completion;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult; + public static final fun CreateElicitationRequest (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequest; + public static final fun CreateElicitationResult (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 CreateElicitationResult$default (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 static final fun CreateMessageRequest (Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest; + public static final fun InitializedNotification (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/InitializedNotification; + public static synthetic fun InitializedNotification$default (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/InitializedNotification; + public static final fun LoggingMessageNotification (Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotification; + public static final fun ProgressNotification (Lio/modelcontextprotocol/kotlin/sdk/types/ProgressNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ProgressNotification; + public static final fun PromptListChangedNotification (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/PromptListChangedNotification; + public static final fun ResourceListChangedNotification (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ResourceListChangedNotification; + public static synthetic fun ResourceListChangedNotification$default (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ResourceListChangedNotification; + public static final fun ResourceUpdatedNotification (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotification; + public static final fun RootsListChangedNotification (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/RootsListChangedNotification; + public static synthetic fun RootsListChangedNotification$default (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/RootsListChangedNotification; + public static final fun 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;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities; + public static synthetic fun ServerCapabilities$default (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;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities; + public static final fun 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;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/Tool; + public static synthetic fun Tool$default (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;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/Tool; + public static final fun ToolListChangedNotification (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolListChangedNotification; + public static final fun getSUPPORTED_PROTOCOL_VERSIONS ()Ljava/util/List; + 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_utilKt { + public static final fun error (Lio/modelcontextprotocol/kotlin/sdk/CallToolResult;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult; + public static synthetic fun error$default (Lio/modelcontextprotocol/kotlin/sdk/CallToolResult;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult; + public static final fun getEmptyJsonObject ()Lkotlinx/serialization/json/JsonObject; + public static final fun ok (Lio/modelcontextprotocol/kotlin/sdk/CallToolResult;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult; + public static synthetic fun ok$default (Lio/modelcontextprotocol/kotlin/sdk/CallToolResult;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult; +} + +public final class io/modelcontextprotocol/kotlin/sdk/UnknownContent { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/UnknownContent$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/UnknownContent; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/UnknownContent;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/UnknownContent; + public fun equals (Ljava/lang/Object;)Z + 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/UnknownContent$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/UnknownContent$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/UnknownContent; + 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 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 fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/Method;Lio/modelcontextprotocol/kotlin/sdk/types/NotificationParams;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/Method;Lio/modelcontextprotocol/kotlin/sdk/types/NotificationParams;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/Method; + public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/NotificationParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/Method;Lio/modelcontextprotocol/kotlin/sdk/types/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/types/NotificationParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification; + public fun equals (Ljava/lang/Object;)Z + public final fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; + public final 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/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 { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/UnknownReference$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/UnknownReference; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/UnknownReference;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/UnknownReference; + public fun equals (Ljava/lang/Object;)Z + 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 fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/UnknownReference; + 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 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 fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/internal/Utils_jvmKt { public static final fun getIODispatcher ()Lkotlinx/coroutines/CoroutineDispatcher; } @@ -59,7 +458,6 @@ public abstract class io/modelcontextprotocol/kotlin/sdk/shared/Protocol { 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; @@ -72,7 +470,6 @@ public abstract class io/modelcontextprotocol/kotlin/sdk/shared/Protocol { 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; @@ -81,23 +478,20 @@ public abstract class io/modelcontextprotocol/kotlin/sdk/shared/Protocol { 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/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 static final fun getMcpJson ()Lkotlinx/serialization/json/Json; } 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 synthetic fun (ZJILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (ZJLkotlin/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 } @@ -112,16 +506,8 @@ 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/RequestHandlerExtra { + public fun ()V } public final class io/modelcontextprotocol/kotlin/sdk/shared/RequestOptions : io/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions { @@ -138,12 +524,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/shared/RequestOptions : io public fun toString ()Ljava/lang/String; } -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 @@ -237,6 +617,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/AudioContent : io/mo 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 get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -275,6 +656,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/BaseNotificationPara 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 getMeta ()Lkotlinx/serialization/json/JsonObject; + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -337,6 +719,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/BlobResourceContents public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public fun getMimeType ()Ljava/lang/String; public fun getUri ()Ljava/lang/String; + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -356,53 +739,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/BlobResourceContents public final fun serializer ()Lkotlinx/serialization/KSerializer; } -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 @@ -416,7 +752,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CallToolRequest : io 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; } @@ -441,26 +776,24 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CallToolRequestBuild 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 synthetic fun build$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 synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;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 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 final fun component3-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun copy-EDEVuBg (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams; + public static synthetic fun copy-EDEVuBg$default (Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams; public fun equals (Ljava/lang/Object;)Z 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; } @@ -494,6 +827,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CallToolResult : io/ public final fun getContent ()Ljava/util/List; public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public final fun getStructuredContent ()Lkotlinx/serialization/json/JsonObject; + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public final fun isError ()Ljava/lang/Boolean; public fun toString ()Ljava/lang/String; @@ -514,67 +848,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CallToolResult$Compa 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/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/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/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/types/CancelTaskRequest$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -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 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 @@ -620,6 +893,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CancelledNotificatio 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 get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -639,32 +913,22 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CancelledNotificatio 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 fun (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;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 ()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 final fun component3 ()Lkotlinx/serialization/json/JsonObject; + public final fun component4 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;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 getElicitation ()Lkotlinx/serialization/json/JsonObject; 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 final fun getSampling ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -681,39 +945,8 @@ public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ClientCapa } 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 getElicitation ()Lkotlinx/serialization/json/JsonObject; + public final fun getSampling ()Lkotlinx/serialization/json/JsonObject; public final fun serializer ()Lkotlinx/serialization/KSerializer; } @@ -746,169 +979,17 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$R 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 elicitation (Lkotlin/jvm/functions/Function1;)V + public final fun elicitation (Lkotlinx/serialization/json/JsonObject;)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 final fun sampling (Lkotlin/jvm/functions/Function1;)V + public final fun sampling (Lkotlinx/serialization/json/JsonObject;)V } public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/ClientNotification : io/modelcontextprotocol/kotlin/sdk/types/Notification { @@ -935,6 +1016,10 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ClientResult$Compani public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/ClientResult$DefaultImpls { + public static fun get_meta (Lio/modelcontextprotocol/kotlin/sdk/types/ClientResult;)Lkotlinx/serialization/json/JsonObject; +} + 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; @@ -945,6 +1030,8 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CommonKt { 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 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 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; @@ -980,7 +1067,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CompleteRequestBuild 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 synthetic fun build$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 @@ -1089,6 +1176,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CompleteResult : io/ 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 get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -1153,6 +1241,10 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ContentBlock$Compani public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/ContentBlock$DefaultImpls { + public static fun get_meta (Lio/modelcontextprotocol/kotlin/sdk/types/ContentBlock;)Lkotlinx/serialization/json/JsonObject; +} + 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; @@ -1160,8 +1252,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ContentTypes : java/ 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; @@ -1190,7 +1280,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest 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; @@ -1214,7 +1303,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest 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 synthetic fun build$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; @@ -1235,12 +1324,9 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest 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 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;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;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; @@ -1248,9 +1334,9 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest 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 final fun component9-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun copy-qtvSJzw (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;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams; + public static synthetic fun copy-qtvSJzw$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;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 @@ -1260,10 +1346,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest 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; } @@ -1285,23 +1368,22 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest 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 synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;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/MediaContent;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 component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent; 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 final fun copy-ctVX1tw (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;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;Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;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 final fun getContent ()Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent; 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 get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -1321,36 +1403,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageResult$ 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 @@ -1408,142 +1460,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CustomRequest$Compan 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 @@ -1579,70 +1495,56 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequest$Compan 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 synthetic fun build$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 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 synthetic fun (Ljava/lang/String;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/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 final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema; + public final fun component3-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun copy-EDEVuBg (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams; + public static synthetic fun copy-EDEVuBg$default (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams; public fun equals (Ljava/lang/Object;)Z - public fun getMessage ()Ljava/lang/String; + public final 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 synthetic class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestFormParams; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams; 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 final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams;)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 (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/types/ElicitRequestParams$RequestedSchema; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lkotlinx/serialization/json/JsonObject;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 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; @@ -1663,43 +1565,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$ 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; @@ -1722,6 +1587,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitResult : io/mo 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 get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -1755,70 +1621,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitResult$Compani 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; } @@ -1837,6 +1639,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/EmbeddedResource : i 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 get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -1866,6 +1669,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/EmptyResult : io/mod 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 get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -1885,46 +1689,11 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/EmptyResult$Companio 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 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 ()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; @@ -1957,7 +1726,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/GetPromptRequest$Com 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 synthetic fun build$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 @@ -2009,6 +1778,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/GetPromptResult : io public final fun getDescription ()Ljava/lang/String; public final fun getMessages ()Ljava/util/List; public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -2028,203 +1798,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/GetPromptResult$Comp 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 ()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 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 @@ -2288,6 +1861,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ImageContent : io/mo 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 get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -2402,7 +1976,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/InitializeRequest$Co 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 synthetic fun build$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; @@ -2463,6 +2037,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/InitializeResult : i 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 get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -2517,44 +2092,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/InitializedNotificat 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 @@ -2719,48 +2256,13 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/JsonUtilsKt { 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 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 ()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; @@ -2792,7 +2294,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListPromptsRequest$C 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 synthetic fun build$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 { @@ -2808,6 +2310,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListPromptsResult : public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public fun getNextCursor ()Ljava/lang/String; public final fun getPrompts ()Ljava/util/List; + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -2832,6 +2335,8 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplate 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 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 ()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; @@ -2863,7 +2368,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplate 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 synthetic fun build$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 { @@ -2879,6 +2384,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplate public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public fun getNextCursor ()Ljava/lang/String; public final fun getResourceTemplates ()Ljava/util/List; + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -2903,6 +2409,8 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourcesRequest 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 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 ()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; @@ -2934,7 +2442,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourcesRequest 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 synthetic fun build$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 { @@ -2950,6 +2458,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourcesResult public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public fun getNextCursor ()Ljava/lang/String; public final fun getResources ()Ljava/util/List; + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -2974,6 +2483,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest : i 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 synthetic fun (Lkotlinx/serialization/json/JsonObject;Lkotlin/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; @@ -3004,7 +2514,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest$Com 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 synthetic fun build$kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; } public final class io/modelcontextprotocol/kotlin/sdk/types/ListRootsResult : io/modelcontextprotocol/kotlin/sdk/types/ClientResult { @@ -3018,6 +2528,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListRootsResult : io public fun equals (Ljava/lang/Object;)Z public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public final fun getRoots ()Ljava/util/List; + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -3037,76 +2548,13 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListRootsResult$Comp 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 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 ()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; @@ -3138,7 +2586,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListToolsRequest$Com 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 synthetic fun build$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 { @@ -3154,6 +2602,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListToolsResult : io public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public fun getNextCursor ()Ljava/lang/String; public final fun getTools ()Ljava/util/List; + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -3236,6 +2685,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotifi 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 get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -3262,8 +2712,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Logging_dslKt { 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 final class io/modelcontextprotocol/kotlin/sdk/types/McpException : java/lang/Exception { 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 @@ -3272,7 +2721,7 @@ public class io/modelcontextprotocol/kotlin/sdk/types/McpException : java/lang/E 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 abstract interface class io/modelcontextprotocol/kotlin/sdk/types/MediaContent : io/modelcontextprotocol/kotlin/sdk/types/ContentBlock { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent$Companion; } @@ -3280,6 +2729,10 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/MediaContent$Compani public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/MediaContent$DefaultImpls { + public static fun get_meta (Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;)Lkotlinx/serialization/json/JsonObject; +} + public abstract class io/modelcontextprotocol/kotlin/sdk/types/MediaContentBuilder { public fun ()V public final fun annotations (Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;)V @@ -3336,7 +2789,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Method$Defined : jav 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; @@ -3344,7 +2796,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Method$Defined : jav 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; @@ -3356,11 +2807,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Method$Defined : jav 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; @@ -3435,14 +2881,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ModelPreferences$Com 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; @@ -3461,12 +2899,8 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/NotificationParams$C 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 final class io/modelcontextprotocol/kotlin/sdk/types/NotificationParams$DefaultImpls { + public static fun get_meta (Lio/modelcontextprotocol/kotlin/sdk/types/NotificationParams;)Lkotlinx/serialization/json/JsonObject; } public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/PaginatedRequest : io/modelcontextprotocol/kotlin/sdk/types/Request { @@ -3530,6 +2964,10 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/PaginatedResult$Comp public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/PaginatedResult$DefaultImpls { + public static fun get_meta (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedResult;)Lkotlinx/serialization/json/JsonObject; +} + 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 @@ -3565,21 +3003,13 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/PingRequest$Companio 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 synthetic fun build$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 @@ -3650,6 +3080,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ProgressNotification public final fun getProgress ()D public final fun getProgressToken ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestId; public final fun getTotal ()Ljava/lang/Double; + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -3688,6 +3119,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Prompt : io/modelcon public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public final fun getName ()Ljava/lang/String; public final fun getTitle ()Ljava/lang/String; + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -3878,14 +3310,13 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/RPCError$ErrorCode { 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 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 ()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; @@ -3917,7 +3348,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequest$ 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 synthetic fun build$kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; public final fun getUri ()Ljava/lang/String; public final fun setUri (Ljava/lang/String;)V } @@ -3963,6 +3394,7 @@ public final class io/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 get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -4005,33 +3437,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ReferenceType$Compan 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; @@ -4125,13 +3530,8 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/RequestMeta { 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; @@ -4184,6 +3584,10 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/RequestResult$Compan public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/RequestResult$DefaultImpls { + public static fun get_meta (Lio/modelcontextprotocol/kotlin/sdk/types/RequestResult;)Lkotlinx/serialization/json/JsonObject; +} + 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 @@ -4209,6 +3613,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Resource : io/modelc public final fun getSize ()Ljava/lang/Long; public final fun getTitle ()Ljava/lang/String; 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; } @@ -4238,6 +3643,10 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceContents$Com public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceContents$DefaultImpls { + public static fun get_meta (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceContents;)Lkotlinx/serialization/json/JsonObject; +} + 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; } @@ -4246,6 +3655,10 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceLike$Compani public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceLike$DefaultImpls { + public static fun get_meta (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceLike;)Lkotlinx/serialization/json/JsonObject; +} + 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 @@ -4272,6 +3685,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceLink : io/mo 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 get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -4345,6 +3759,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate : i public final fun getName ()Ljava/lang/String; public final fun getTitle ()Ljava/lang/String; public final fun getUriTemplate ()Ljava/lang/String; + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -4432,6 +3847,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotif 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; } @@ -4485,6 +3901,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Root : io/modelconte public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public final fun getName ()Ljava/lang/String; 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; } @@ -4541,18 +3958,13 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Roots_dslKt { 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 fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;)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 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 final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;)Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessage; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessage;Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessage; public fun equals (Ljava/lang/Object;)Z - public final fun getContent ()Ljava/util/List; - public final fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public final fun getContent ()Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent; public final fun getRole ()Lio/modelcontextprotocol/kotlin/sdk/types/Role; public fun hashCode ()I public fun toString ()Ljava/lang/String; @@ -4580,15 +3992,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/SamplingMessageBuild 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 @@ -4604,26 +4007,22 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Sampling_dslKt { 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 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;Lkotlinx/serialization/json/JsonObject;)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;Lkotlinx/serialization/json/JsonObject;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 final fun component6 ()Lkotlinx/serialization/json/JsonObject; + 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;Lkotlinx/serialization/json/JsonObject;)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;Lkotlinx/serialization/json/JsonObject;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; @@ -4706,97 +4105,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$R public final fun serializer ()Lkotlinx/serialization/KSerializer; } -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 @@ -4850,6 +4158,10 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ServerResult$Compani public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/ServerResult$DefaultImpls { + public static fun get_meta (Lio/modelcontextprotocol/kotlin/sdk/types/ServerResult;)Lkotlinx/serialization/json/JsonObject; +} + 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 @@ -4884,7 +4196,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/SetLevelRequest$Comp 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 synthetic fun build$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 } @@ -4919,14 +4231,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/SetLevelRequestParam 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; @@ -4957,68 +4261,14 @@ 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/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;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 ()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 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/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/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/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/types/StringSchema$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -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 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 ()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; @@ -5050,7 +4300,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest$Com 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 synthetic fun build$kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; public final fun getUri ()Ljava/lang/String; public final fun setUri (Ljava/lang/String;)V } @@ -5085,192 +4335,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/SubscribeRequestPara 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 @@ -5285,6 +4349,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/TextContent : io/mod 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 get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -5326,6 +4391,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/TextResourceContents public fun getMimeType ()Ljava/lang/String; public final fun getText ()Ljava/lang/String; public fun getUri ()Ljava/lang/String; + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -5345,111 +4411,10 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/TextResourceContents public final fun serializer ()Lkotlinx/serialization/KSerializer; } -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/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 getDefault ()Ljava/util/List; - public final fun getDescription ()Ljava/lang/String; - 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 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;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;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; @@ -5457,20 +4422,19 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Tool : io/modelconte 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 final fun component8 ()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;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;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/types/ToolSchema; public final fun getTitle ()Ljava/lang/String; + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -5527,78 +4491,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations$Comp public final fun serializer ()Lkotlinx/serialization/KSerializer; } -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/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 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/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/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/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/types/ToolChoice$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -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 (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 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/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/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/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/types/ToolExecution$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - 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 @@ -5630,59 +4522,20 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ToolListChangedNotif 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 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 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/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/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/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/types/ToolResultContent$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - 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 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 (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lkotlinx/serialization/json/JsonObject; + public final fun component2 ()Ljava/util/List; + public final fun component3 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (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;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 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; @@ -5703,41 +4556,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ToolSchema$Companion public final fun serializer ()Lkotlinx/serialization/KSerializer; } -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 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; @@ -5763,6 +4581,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/UnknownResourceConte public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public fun getMimeType ()Ljava/lang/String; public fun getUri ()Ljava/lang/String; + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -5818,7 +4637,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequest$C public final class io/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/RequestBuilder { public fun ()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 synthetic fun build$kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; public final fun getUri ()Ljava/lang/String; public final fun setUri (Ljava/lang/String;)V } @@ -5853,209 +4672,17 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequestPa public final fun serializer ()Lkotlinx/serialization/KSerializer; } -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 fun get_meta ()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; +public final class io/modelcontextprotocol/kotlin/sdk/types/WithMeta$DefaultImpls { + public static fun get_meta (Lio/modelcontextprotocol/kotlin/sdk/types/WithMeta;)Lkotlinx/serialization/json/JsonObject; } diff --git a/kotlin-sdk-core/build.gradle.kts b/kotlin-sdk-core/build.gradle.kts index 1d19f5d..0e702c4 100644 --- a/kotlin-sdk-core/build.gradle.kts +++ b/kotlin-sdk-core/build.gradle.kts @@ -4,10 +4,65 @@ 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) + alias(libs.plugins.openapi.generator) +} + +/* +tasks.withType>().configureEach { + dependsOn(tasks.openApiGenerate) +} + +tasks.named("runKtlintCheckOverCommonMainSourceSet") { + dependsOn(tasks.openApiGenerate) +} + +// Also ensure it runs before other relevant tasks +tasks.withType().configureEach { + dependsOn(tasks.openApiGenerate) +} + +tasks.withType().configureEach { + if (name.lowercase().contains("sourcesjar")) { + dependsOn(tasks.openApiGenerate) + } +} + */ + +openApiGenerate { + val schemaVersion = "2025-03-26" // or "2025-06-18" or "draft" + val schemaUrl = +// "https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/refs/heads/main/schema/$schemaVersion/schema.json" + "https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/refs/tags/$schemaVersion/schema/$schemaVersion/schema.json" + generatorName = "kotlin" + remoteInputSpec = schemaUrl + outputDir = layout.buildDirectory.dir("generated-sources/openapi").get().asFile.absolutePath + packageName = "io.modelcontextprotocol.kotlin.sdk.models" + modelPackage = "io.modelcontextprotocol.kotlin.sdk.models" + apiPackage = "io.modelcontextprotocol.kotlin.sdk.api" + generateModelTests = false + generateModelDocumentation = false + cleanupOutput = false + skipValidateSpec = true // do not validate spec + library.set("multiplatform") + ignoreFileOverride.set("${layout.projectDirectory}/.openapi-generator-ignore") + globalProperties.set( + mapOf( + "supportingFiles" to "", + "models" to "", // or generate all models + ), + ) + configOptions.set( + mapOf( + "omitGradleWrapper" to "true", + "enumPropertyNaming" to "UPPERCASE", + "dateLibrary" to "kotlinx-datetime", + "explicitApi" to "true", + "modelMutable" to "false", + ), + ) } // Generation library versions @@ -36,8 +91,10 @@ kotlin { iosArm64() iosX64() iosSimulatorArm64() + watchosX64() watchosArm64() watchosSimulatorArm64() + tvosX64() tvosArm64() tvosSimulatorArm64() js { diff --git a/kotlin-sdk-core/detekt-baseline-commonMainSourceSet.xml b/kotlin-sdk-core/detekt-baseline-commonMainSourceSet.xml new file mode 100644 index 0000000..d774a5b --- /dev/null +++ b/kotlin-sdk-core/detekt-baseline-commonMainSourceSet.xml @@ -0,0 +1,27 @@ + + + + + CyclomaticComplexMethod:jsonUtils.kt:@OptIn(ExperimentalUnsignedTypes::class, ExperimentalSerializationApi::class) private fun convertToJsonElement: JsonElement + LongMethod:Protocol.kt:Protocol$public suspend fun <T : RequestResult> request: T + MagicNumber:completion.kt:CompleteResult.Completion$100 + MatchingDeclarationName:capabilities.dsl.kt:ClientCapabilitiesBuilder + MatchingDeclarationName:methods.kt:Method + MaxLineLength:AbstractClientTransport.kt:AbstractClientTransport$* + MaxLineLength:ClientTransportState.kt:ClientTransportState.Initializing$* + MaxLineLength:Protocol.kt:Protocol$"Received progress notification: token=${notification.params.progressToken}, progress=${notification.params.progress}/${notification.params.total}" + MaxLineLength:Protocol.kt:Protocol$* + MaxLineLength:Protocol.kt:RequestOptions$* + MaxLineLength:Transport.kt:Transport$* + MaxLineLength:TransportSendOptions.kt:TransportSendOptions$* + ReturnCount:ReadBuffer.kt:ReadBuffer$public fun readMessage: JSONRPCMessage? + ThrowsCount:AbstractClientTransport.kt:AbstractClientTransport$public override suspend fun send + TooGenericExceptionCaught:AbstractClientTransport.kt:AbstractClientTransport$e: Exception + TooGenericExceptionCaught:Protocol.kt:Protocol$cause: Throwable + TooGenericExceptionCaught:Protocol.kt:Protocol$error: Throwable + TooGenericExceptionCaught:Protocol.kt:Protocol$sendError: Throwable + TooGenericExceptionCaught:ReadBuffer.kt:ReadBuffer$e: Exception + TooGenericExceptionCaught:WebSocketMcpTransport.kt:WebSocketMcpTransport$e: Exception + TooManyFunctions:Protocol.kt:Protocol + + diff --git a/kotlin-sdk-core/detekt-baseline-main.xml b/kotlin-sdk-core/detekt-baseline-main.xml index d483d7e..a22fbcd 100644 --- a/kotlin-sdk-core/detekt-baseline-main.xml +++ b/kotlin-sdk-core/detekt-baseline-main.xml @@ -2,6 +2,31 @@ + CyclomaticComplexMethod:jsonUtils.kt:@OptIn(ExperimentalUnsignedTypes::class, ExperimentalSerializationApi::class) private fun convertToJsonElement: JsonElement + InjectDispatcher:utils.jvm.kt:IO + InstanceOfCheckForException:AbstractClientTransport.kt:AbstractClientTransport$e is McpException + LongMethod:Protocol.kt:Protocol$public suspend fun <T : RequestResult> request: T + MagicNumber:completion.kt:CompleteResult.Completion$100 + MatchingDeclarationName:capabilities.dsl.kt:ClientCapabilitiesBuilder + MatchingDeclarationName:methods.kt:Method + MaxLineLength:AbstractClientTransport.kt:AbstractClientTransport$* + MaxLineLength:ClientTransportState.kt:ClientTransportState.Initializing$* + MaxLineLength:Protocol.kt:Protocol$"Received progress notification: token=${notification.params.progressToken}, progress=${notification.params.progress}/${notification.params.total}" + MaxLineLength:Protocol.kt:Protocol$* + MaxLineLength:Protocol.kt:RequestOptions$* + MaxLineLength:Transport.kt:Transport$* + MaxLineLength:TransportSendOptions.kt:TransportSendOptions$* + NoNameShadowing:Protocol.kt:Protocol$error + ReturnCount:ReadBuffer.kt:ReadBuffer$public fun readMessage: JSONRPCMessage? + ThrowsCount:AbstractClientTransport.kt:AbstractClientTransport$public override suspend fun send + TooGenericExceptionCaught:AbstractClientTransport.kt:AbstractClientTransport$e: Exception + TooGenericExceptionCaught:Protocol.kt:Protocol$cause: Throwable + TooGenericExceptionCaught:Protocol.kt:Protocol$error: Throwable + TooGenericExceptionCaught:Protocol.kt:Protocol$sendError: Throwable + TooGenericExceptionCaught:ReadBuffer.kt:ReadBuffer$e: Exception + TooGenericExceptionCaught:WebSocketMcpTransport.kt:WebSocketMcpTransport$e: Exception + TooManyFunctions:Protocol.kt:Protocol UnsafeCallOnNullableType:Protocol.kt:Protocol$response!! + UseCheckNotNull:Protocol.kt:Protocol$check(error != null) 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 37a57b7..d1cbe78 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,5 +2,4 @@ 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 index 27013d3..5f67999 100644 --- 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 @@ -99,10 +99,8 @@ public abstract class AbstractClientTransport : AbstractTransport() { * 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. + * @param expected The expected transport state. An exception will be thrown if the actual state does not match this value. + * @throws IllegalStateException If the current state does not match the expected state. */ protected fun checkState( expected: ClientTransportState, 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 index a738289..7b88100 100644 --- 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 @@ -6,13 +6,11 @@ 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. + * Implements [onClose], [onError] and [onMessage] functions of [Transport] providing + * corresponding [_onClose], [_onError] and [_onMessage] properties to use for an implementation. */ @OptIn(ExperimentalAtomicApi::class) +@Suppress("PropertyName") public abstract class AbstractTransport : Transport { private val onCloseCalled = AtomicBoolean(false) private var _onClose: (() -> Unit) = {} 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 908e524..e9d833a 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 @@ -27,41 +27,23 @@ 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.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.coroutines.TimeoutCancellationException +import kotlinx.coroutines.withTimeout +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.ClassDiscriminatorMode +import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.encodeToJsonElement -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 { } -/** Default implementation name used in MCP handshake. */ public const val IMPLEMENTATION_NAME: String = "mcp-ktor" /** @@ -69,29 +51,40 @@ public const val IMPLEMENTATION_NAME: String = "mcp-ktor" */ public typealias ProgressCallback = (Progress) -> Unit +@OptIn(ExperimentalSerializationApi::class) +@Deprecated( + message = "Use McpJson instead", + replaceWith = ReplaceWith("McpJson", "io.modelcontextprotocol.kotlin.sdk.types.McpJson"), + level = DeprecationLevel.ERROR, +) +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, ) /** @@ -99,27 +92,21 @@ 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 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. * @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. + * If set, requests progress notifications from the remote end (if supported). + * When progress notifications are received, this callback will be 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. + * If exceeded, a McpException with code `RequestTimeout` will be raised from request(). + * If not specified, `DEFAULT_REQUEST_TIMEOUT` will be used as the timeout. */ public class RequestOptions( relatedRequestId: RequestId? = null, @@ -128,13 +115,9 @@ public class RequestOptions( 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 - - /** 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, @@ -155,7 +138,7 @@ public class RequestOptions( override fun hashCode(): Int { var result = super.hashCode() - result = 31 * result + onProgress.hashCode() + result = 31 * result + (onProgress?.hashCode() ?: 0) result = 31 * result + timeout.hashCode() return result } @@ -166,88 +149,22 @@ public class RequestOptions( /** * 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 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] +public class 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?) { - private val connectionRef: AtomicRef = atomic(null) - - /** The active transport, or `null` if not connected. */ - public val transport: Transport? - get() = connectionRef.value?.transport + public var transport: Transport? = null + private set private val _requestHandlers: AtomicRef RequestResult?>> = atomic(persistentMapOf()) - - /** Registered request handlers keyed by method name. */ public val requestHandlers: Map< String, suspend ( @@ -259,41 +176,20 @@ 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> = atomic(persistentMapOf()) - - /** 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. * @@ -323,11 +219,6 @@ 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 @@ -338,64 +229,15 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio } } - /** - * 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. - * - * @throws IllegalStateException if this Protocol is already connected; call [close] first. + * 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. */ public open suspend fun connect(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") - } - + this.transport = transport transport.onClose { - doClose(connection) + doClose() } transport.onError { @@ -403,43 +245,24 @@ 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 -> dispatchRequest(connection, message) - is JSONRPCNotification -> dispatchNotification(connection, message) + is JSONRPCRequest -> onRequest(message) + is JSONRPCNotification -> onNotification(message) is JSONRPCError -> onResponse(null, message) is JSONRPCEmptyMessage -> Unit } } 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 - } + transport.start() } - 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() + private fun doClose() { + val handlersToNotify = _responseHandlers.value.values.toList() + _responseHandlers.getAndSet(persistentMapOf()) _progressHandlers.getAndSet(persistentMapOf()) - recentlyCancelledRequestIds.getAndSet(persistentListOf()) - connection.inFlightRequestJobs.getAndSet(persistentMapOf()) - connection.handlerScope.cancel() + transport = null onClose() val error = McpException(RPCError.ErrorCode.CONNECTION_CLOSED, "Connection closed") @@ -448,110 +271,6 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio } } - 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 { - 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) { "Failed to send busy rejection for request id: ${request.id}" } - onError(cause) - } - } - - 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}" } @@ -563,24 +282,21 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio } 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) { + private suspend fun onRequest(request: JSONRPCRequest) { 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}" } try { - capturedTransport.send( + transport?.send( JSONRPCError( id = request.id, error = RPCError( @@ -589,8 +305,6 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio ), ), ) - } catch (e: CancellationException) { - throw e } catch (cause: Throwable) { logger.error(cause) { "Error sending method not found response" } onError(cause) @@ -598,80 +312,38 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio return } - val extra = RequestHandlerExtra( - requestId = request.id, - method = Method.from(request.method), - protocol = this, - capturedTransport = capturedTransport, - ) + try { + val result = handler(request, RequestHandlerExtra()) + logger.trace { "Request handled successfully: ${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 + transport?.send( + JSONRPCResponse( + id = request.id, + result = result ?: EmptyResult(), + ), + ) + } catch (cause: Throwable) { + logger.error(cause) { "Error handling request: ${request.method} (id: ${request.id})" } - withContext(extra) { try { - 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( + transport?.send( + JSONRPCError( id = request.id, - result = result ?: EmptyResult(), + error = RPCError( + code = RPCError.ErrorCode.INTERNAL_ERROR, + message = cause.message ?: "Internal error", + ), ), ) - } 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 + } catch (sendError: Throwable) { + logger.error(sendError) { + "Failed to send error response for request: ${request.method} (id: ${request.id})" } - respondWithError(capturedTransport, request, cause) + // Optionally implement fallback behavior here } } } - 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 { "Received progress notification: token=${notification.params.progressToken}, progress=${notification.params.progress}/${notification.params.total}" @@ -683,10 +355,6 @@ 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)}", ) @@ -703,7 +371,7 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio val oldResponseHandlers = _responseHandlers.getAndUpdate { current -> if (messageId != null && messageId in current) { - current.removing(messageId) + current.remove(messageId) } else { current } @@ -712,12 +380,8 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio val handler = oldResponseHandlers[messageId] if (handler != null) { - messageId?.let { msg -> _progressHandlers.update { it.removing(msg) } } + messageId?.let { msg -> _progressHandlers.update { it.remove(msg) } } } else { - 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)}", @@ -729,8 +393,8 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio if (response != null) { handler(response, null) } else { - checkNotNull(error) - val mcpException = McpException.fromError( + check(error != null) + val mcpException = McpException( code = error.error.code, message = error.error.message, data = error.error.data, @@ -775,8 +439,7 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio public suspend fun request(request: Request, options: RequestOptions? = null): T { logger.trace { "Sending request: ${request.method}" } val result = CompletableDeferred() - val connection = connectionRef.value ?: error("Not connected") - val transport = connection.transport + val transport = transport ?: error("Not connected") if (this@Protocol.options?.enforceStrictCapabilities == true) { assertCapabilityForMethod(request.method) @@ -786,7 +449,7 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio options?.onProgress?.let { progressHandler -> logger.trace { "Registering progress handler for request id: $id" } _progressHandlers.update { current -> - current.putting(id, progressHandler) + current.put(id, progressHandler) } val paramsObject = (this.params as? JsonObject) ?: JsonObject(emptyMap()) @@ -805,99 +468,57 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio val jsonRpcRequestId = jsonRpcRequest.id _responseHandlers.update { current -> - current.putting(jsonRpcRequestId) { response, error -> + current.put(jsonRpcRequestId) { response, error -> if (error != null) { result.completeExceptionally(error) - return@putting + return@put } try { @Suppress("UNCHECKED_CAST") result.complete(response!!.result as T) - } catch (e: Throwable) { - result.completeExceptionally(e) + } catch (error: Throwable) { + result.completeExceptionally(error) } } } - 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 cancel: suspend (Throwable) -> Unit = { reason: Throwable -> + _responseHandlers.update { current -> current.remove(jsonRpcRequestId) } + _progressHandlers.update { current -> current.remove(jsonRpcRequestId) } - 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 notification = CancelledNotification( + params = CancelledNotificationParams( + requestId = jsonRpcRequestId, + reason = reason.message ?: "Unknown", + ), + ) - if (notifyPeer) { - val notification = CancelledNotification( - params = CancelledNotificationParams( - requestId = jsonRpcRequestId, - reason = reason.message ?: "Unknown", - ), - ) - transport.send(notification.toJSON(), options) - } + val jsonRpcNotification = notification.toJSON() + + transport.send(jsonRpcNotification, 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 { - val response = withTimeoutOrNull(timeout) { + withTimeout(timeout) { logger.trace { "Sending request message with id: $jsonRpcRequestId" } - transport.send(jsonRpcRequest, options) - result.await() + this@Protocol.transport?.send(jsonRpcRequest, options) } - 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( + return result.await() + } catch (cause: TimeoutCancellationException) { + logger.error { "Request timed out after ${timeout.inWholeMilliseconds}ms: ${request.method}" } + cancel( + 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) - } - } + ), + ) + 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) } } } @@ -918,11 +539,6 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio * 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, @@ -938,60 +554,31 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio block: suspend (T, RequestHandlerExtra) -> RequestResult?, ) { assertRequestHandlerCapability(method) - val wrapped = wrapRequestHandler(method, block) _requestHandlers.update { current -> - 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) + current.put(method.value) { jSONRPCRequest, extraHandler -> + val request = jSONRPCRequest.fromJSON() + val response = block(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.removing(method.value) } + _requestHandlers.update { current -> current.remove(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.putting(method.value) { + current.put(method.value) { @Suppress("UNCHECKED_CAST") handler(it.fromJSON() as T) } @@ -1002,22 +589,6 @@ 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.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()) + _notificationHandlers.update { current -> current.remove(method.value) } } } 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 8102cf6..d3fc61f 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 @@ -4,30 +4,18 @@ import io.github.oshai.kotlinlogging.KotlinLogging 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 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 - } +public class ReadBuffer { 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) } @@ -50,6 +38,7 @@ public class ReadBuffer internal constructor(private val maxFrameSize: Int) { val line = readNextLine() ?: return null if (line.isBlank()) continue + @Suppress("TooGenericExceptionCaught") val message = try { deserializeMessage(line) } catch (e: Exception) { @@ -64,11 +53,7 @@ public class ReadBuffer internal constructor(private val maxFrameSize: Int) { 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) + if (lfIndex == -1L) return null return if (lfIndex == 0L) { buffer.skip(1) @@ -86,20 +71,13 @@ public class ReadBuffer internal constructor(private val maxFrameSize: Int) { } } - 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 a89c102..cb57547 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 @@ -21,9 +21,9 @@ 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 + * @property message The JSON-RPC message to send, either a request or a response. + * @property options Optional transport-specific options that control sending behavior. + * Different transport implementations may support different options. */ public suspend fun send(message: JSONRPCMessage, options: TransportSendOptions? = null) 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 index 5490288..c56df04 100644 --- 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 @@ -17,16 +17,10 @@ public open class TransportSendOptions( 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, 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 baf3723..de75cc4 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 @@ -16,9 +16,7 @@ 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 {} @@ -75,8 +73,6 @@ 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 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 new file mode 100644 index 0000000..1b43482 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt @@ -0,0 +1,2250 @@ +@file:Suppress("FunctionName", "TooManyFunctions") + +package io.modelcontextprotocol.kotlin.sdk + +import io.modelcontextprotocol.kotlin.sdk.types.ContentBlock +import io.modelcontextprotocol.kotlin.sdk.types.fromJSON +import io.modelcontextprotocol.kotlin.sdk.types.toJSON +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities.Roots as TypesRoots + +@Deprecated( + message = "Use `LATEST_PROTOCOL_VERSION` instead", + replaceWith = ReplaceWith( + "LATEST_PROTOCOL_VERSION", + "io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION", + ), + level = DeprecationLevel.ERROR, +) +public const val LATEST_PROTOCOL_VERSION: String = io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION + +@Deprecated( + message = "Use `SUPPORTED_PROTOCOL_VERSIONS` instead", + replaceWith = ReplaceWith( + "SUPPORTED_PROTOCOL_VERSIONS", + "io.modelcontextprotocol.kotlin.sdk.types.SUPPORTED_PROTOCOL_VERSIONS", + ), + level = DeprecationLevel.ERROR, +) +public val SUPPORTED_PROTOCOL_VERSIONS: List = + io.modelcontextprotocol.kotlin.sdk.types.SUPPORTED_PROTOCOL_VERSIONS + +@Deprecated( + message = "Use `JSONRPC_VERSION` instead", + replaceWith = ReplaceWith("JSONRPC_VERSION", "io.modelcontextprotocol.kotlin.sdk.types.JSONRPC_VERSION"), + level = DeprecationLevel.ERROR, +) +public const val JSONRPC_VERSION: String = io.modelcontextprotocol.kotlin.sdk.types.JSONRPC_VERSION + +/** + * A progress token, used to associate progress notifications with the original request. + * Stores message ID. + */ +@Deprecated( + message = "Use `ProgressToken` instead", + replaceWith = ReplaceWith("ProgressToken", "io.modelcontextprotocol.kotlin.sdk.types.ProgressToken"), + level = DeprecationLevel.ERROR, +) +public typealias ProgressToken = io.modelcontextprotocol.kotlin.sdk.types.ProgressToken + +/** + * An opaque token used to represent a cursor for pagination. + */ +@Deprecated( + message = "This alias will be removed. Use String directly instead.", + replaceWith = ReplaceWith("String"), + level = DeprecationLevel.ERROR, +) +public typealias Cursor = String + +/** + * Represents an entity that includes additional metadata in its responses. + */ +@Deprecated( + message = "Use `WithMeta` instead", + replaceWith = ReplaceWith("WithMeta", "io.modelcontextprotocol.kotlin.sdk.types.WithMeta"), + level = DeprecationLevel.ERROR, +) +public typealias WithMeta = io.modelcontextprotocol.kotlin.sdk.types.WithMeta + +/** + * An implementation of [WithMeta] containing custom metadata. + */ +@Deprecated( + message = "This class will be removed. Use `WithMeta` instead", + replaceWith = ReplaceWith("WithMeta", "io.modelcontextprotocol.kotlin.sdk.types.WithMeta"), + level = DeprecationLevel.ERROR, +) +public typealias CustomMeta = io.modelcontextprotocol.kotlin.sdk.types.WithMeta + +/** + * Represents a method in the protocol, which can be predefined or custom. + */ +@Deprecated( + message = "Use `Method` instead", + replaceWith = ReplaceWith("Method", "io.modelcontextprotocol.kotlin.sdk.types.Method"), + level = DeprecationLevel.ERROR, +) +public sealed interface Method { + public val value: String + + public companion object { + /** + * Represents a custom method defined by the user. + */ + @Deprecated( + message = "Use `Method.Custom` instead", + replaceWith = ReplaceWith("Method.Custom", "io.modelcontextprotocol.kotlin.sdk.types.Method.Custom"), + level = DeprecationLevel.ERROR, + ) + public fun Custom(value: String): io.modelcontextprotocol.kotlin.sdk.types.Method.Custom = + io.modelcontextprotocol.kotlin.sdk.types.Method.Custom(value) + } + + public object Defined { + @Deprecated( + message = "Use `Method.Defined.Initialize` instead", + replaceWith = ReplaceWith( + "Method.Defined.Initialize", + "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.Initialize", + ), + level = DeprecationLevel.ERROR, + ) + public val Initialize: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = + io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.Initialize + + @Deprecated( + message = "Use `Method.Defined.Ping` instead", + replaceWith = ReplaceWith( + "Method.Defined.Ping", + "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.Ping", + ), + level = DeprecationLevel.ERROR, + ) + public val Ping: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = + io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.Ping + + @Deprecated( + message = "Use `Method.Defined.ResourcesList` instead", + replaceWith = ReplaceWith( + "Method.Defined.ResourcesList", + "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ResourcesList", + ), + level = DeprecationLevel.ERROR, + ) + public val ResourcesList: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = + io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ResourcesList + + @Deprecated( + message = "Use `Method.Defined.ResourcesTemplatesList` instead", + replaceWith = ReplaceWith( + "Method.Defined.ResourcesTemplatesList", + "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ResourcesTemplatesList", + ), + level = DeprecationLevel.ERROR, + ) + public val ResourcesTemplatesList: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = + io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ResourcesTemplatesList + + @Deprecated( + message = "Use `Method.Defined.ResourcesRead` instead", + replaceWith = ReplaceWith( + "Method.Defined.ResourcesRead", + "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ResourcesRead", + ), + level = DeprecationLevel.ERROR, + ) + public val ResourcesRead: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = + io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ResourcesRead + + @Deprecated( + message = "Use `Method.Defined.ResourcesSubscribe` instead", + replaceWith = ReplaceWith( + "Method.Defined.ResourcesSubscribe", + "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ResourcesSubscribe", + ), + level = DeprecationLevel.ERROR, + ) + public val ResourcesSubscribe: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = + io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ResourcesSubscribe + + @Deprecated( + message = "Use `Method.Defined.ResourcesUnsubscribe` instead", + replaceWith = ReplaceWith( + "Method.Defined.ResourcesUnsubscribe", + "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ResourcesUnsubscribe", + ), + level = DeprecationLevel.ERROR, + ) + public val ResourcesUnsubscribe: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = + io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ResourcesUnsubscribe + + @Deprecated( + message = "Use `Method.Defined.PromptsList` instead", + replaceWith = ReplaceWith( + "Method.Defined.PromptsList", + "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.PromptsList", + ), + level = DeprecationLevel.ERROR, + ) + public val PromptsList: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = + io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.PromptsList + + @Deprecated( + message = "Use `Method.Defined.PromptsGet` instead", + replaceWith = ReplaceWith( + "Method.Defined.PromptsGet", + "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.PromptsGet", + ), + level = DeprecationLevel.ERROR, + ) + public val PromptsGet: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = + io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.PromptsGet + + @Deprecated( + message = "Use `Method.Defined.NotificationsCancelled` instead", + replaceWith = ReplaceWith( + "Method.Defined.NotificationsCancelled", + "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsCancelled", + ), + level = DeprecationLevel.ERROR, + ) + public val NotificationsCancelled: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = + io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsCancelled + + @Deprecated( + message = "Use `Method.Defined.NotificationsInitialized` instead", + replaceWith = ReplaceWith( + "Method.Defined.NotificationsInitialized", + "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsInitialized", + ), + level = DeprecationLevel.ERROR, + ) + public val NotificationsInitialized: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = + io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsInitialized + + @Deprecated( + message = "Use `Method.Defined.NotificationsProgress` instead", + replaceWith = ReplaceWith( + "Method.Defined.NotificationsProgress", + "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsProgress", + ), + level = DeprecationLevel.ERROR, + ) + public val NotificationsProgress: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = + io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsProgress + + @Deprecated( + message = "Use `Method.Defined.NotificationsMessage` instead", + replaceWith = ReplaceWith( + "Method.Defined.NotificationsMessage", + "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsMessage", + ), + level = DeprecationLevel.ERROR, + ) + public val NotificationsMessage: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = + io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsMessage + + @Deprecated( + message = "Use `Method.Defined.NotificationsResourcesUpdated` instead", + replaceWith = ReplaceWith( + "Method.Defined.NotificationsResourcesUpdated", + "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsResourcesUpdated", + ), + level = DeprecationLevel.ERROR, + ) + public val NotificationsResourcesUpdated: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = + io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsResourcesUpdated + + @Deprecated( + message = "Use `Method.Defined.NotificationsResourcesListChanged` instead", + replaceWith = ReplaceWith( + "Method.Defined.NotificationsResourcesListChanged", + "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsResourcesListChanged", + ), + level = DeprecationLevel.ERROR, + ) + public val NotificationsResourcesListChanged: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = + io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsResourcesListChanged + + @Deprecated( + message = "Use `Method.Defined.NotificationsToolsListChanged` instead", + replaceWith = ReplaceWith( + "Method.Defined.NotificationsToolsListChanged", + "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsToolsListChanged", + ), + level = DeprecationLevel.ERROR, + ) + public val NotificationsToolsListChanged: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = + io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsToolsListChanged + + @Deprecated( + message = "Use `Method.Defined.NotificationsRootsListChanged` instead", + replaceWith = ReplaceWith( + "Method.Defined.NotificationsRootsListChanged", + "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsRootsListChanged", + ), + level = DeprecationLevel.ERROR, + ) + public val NotificationsRootsListChanged: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = + io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsRootsListChanged + + @Deprecated( + message = "Use `Method.Defined.NotificationsPromptsListChanged` instead", + replaceWith = ReplaceWith( + "Method.Defined.NotificationsPromptsListChanged", + "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsPromptsListChanged", + ), + level = DeprecationLevel.ERROR, + ) + public val NotificationsPromptsListChanged: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = + io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsPromptsListChanged + + @Deprecated( + message = "Use `Method.Defined.ToolsList` instead", + replaceWith = ReplaceWith( + "Method.Defined.ToolsList", + "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ToolsList", + ), + level = DeprecationLevel.ERROR, + ) + public val ToolsList: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = + io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ToolsList + + @Deprecated( + message = "Use `Method.Defined.ToolsCall` instead", + replaceWith = ReplaceWith( + "Method.Defined.ToolsCall", + "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ToolsCall", + ), + level = DeprecationLevel.ERROR, + ) + public val ToolsCall: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = + io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ToolsCall + + @Deprecated( + message = "Use `Method.Defined.LoggingSetLevel` instead", + replaceWith = ReplaceWith( + "Method.Defined.LoggingSetLevel", + "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.LoggingSetLevel", + ), + level = DeprecationLevel.ERROR, + ) + public val LoggingSetLevel: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = + io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.LoggingSetLevel + + @Deprecated( + message = "Use `Method.Defined.SamplingCreateMessage` instead", + replaceWith = ReplaceWith( + "Method.Defined.SamplingCreateMessage", + "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.SamplingCreateMessage", + ), + level = DeprecationLevel.ERROR, + ) + public val SamplingCreateMessage: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = + io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.SamplingCreateMessage + + @Deprecated( + message = "Use `Method.Defined.CompletionComplete` instead", + replaceWith = ReplaceWith( + "Method.Defined.CompletionComplete", + "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.CompletionComplete", + ), + level = DeprecationLevel.ERROR, + ) + public val CompletionComplete: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = + io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.CompletionComplete + + @Deprecated( + message = "Use `Method.Defined.RootsList` instead", + replaceWith = ReplaceWith( + "Method.Defined.RootsList", + "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.RootsList", + ), + level = DeprecationLevel.ERROR, + ) + public val RootsList: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = + io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.RootsList + + @Deprecated( + message = "Use `Method.Defined.ElicitationCreate` instead", + replaceWith = ReplaceWith( + "Method.Defined.ElicitationCreate", + "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ElicitationCreate", + ), + level = DeprecationLevel.ERROR, + ) + public val ElicitationCreate: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = + io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ElicitationCreate + } +} + +/** + * Represents a request in the protocol. + */ +@Deprecated( + message = "Use `Request` instead", + replaceWith = ReplaceWith("Request", "io.modelcontextprotocol.kotlin.sdk.types.Request"), + level = DeprecationLevel.ERROR, +) +public typealias Request = io.modelcontextprotocol.kotlin.sdk.types.Request + +/** + * Converts the request to a JSON-RPC request. + * + * @return The JSON-RPC request representation. + */ +@Deprecated( + message = "Use `toJSON` instead", + replaceWith = ReplaceWith("toJSON", "io.modelcontextprotocol.kotlin.sdk.types.toJSON"), + level = DeprecationLevel.ERROR, +) +@Suppress("DEPRECATION_ERROR") +public fun Request.toJSON(): JSONRPCRequest = this.toJSON() + +/** + * Decodes a JSON-RPC request into a protocol-specific [Request]. + * + * @return The decoded [Request] or null + */ +@Deprecated( + message = "Use `fromJSON` instead", + replaceWith = ReplaceWith("fromJSON", "io.modelcontextprotocol.kotlin.sdk.types.fromJSON"), + level = DeprecationLevel.ERROR, +) +@Suppress("DEPRECATION_ERROR") +internal fun JSONRPCRequest.fromJSON(): Request = this.fromJSON() + +/** + * A custom request with a specified method. + */ +@Deprecated( + message = "Use `CustomRequest` instead", + replaceWith = ReplaceWith("CustomRequest", "io.modelcontextprotocol.kotlin.sdk.types.CustomRequest"), + level = DeprecationLevel.ERROR, +) +public typealias CustomRequest = io.modelcontextprotocol.kotlin.sdk.types.CustomRequest + +/** + * Represents a notification in the protocol. + */ +@Deprecated( + message = "Use `Notification` instead", + replaceWith = ReplaceWith("Notification", "io.modelcontextprotocol.kotlin.sdk.types.Notification"), + level = DeprecationLevel.ERROR, +) +public typealias Notification = io.modelcontextprotocol.kotlin.sdk.types.Notification + +/** + * Converts the notification to a JSON-RPC notification. + * + * @return The JSON-RPC notification representation. + */ +@Deprecated( + message = "Use `toJSON` instead", + replaceWith = ReplaceWith("toJSON", "io.modelcontextprotocol.kotlin.sdk.types.toJSON"), + level = DeprecationLevel.ERROR, +) +@Suppress("DEPRECATION_ERROR") +public fun Notification.toJSON(): JSONRPCNotification = this.toJSON() + +/** + * Decodes a JSON-RPC notification into a protocol-specific [Notification]. + * + * @return The decoded [Notification]. + */ +@Deprecated( + message = "Use `fromJSON` instead", + replaceWith = ReplaceWith("toJSON", "io.modelcontextprotocol.kotlin.sdk.types.fromJSON"), + level = DeprecationLevel.ERROR, +) +@Suppress("DEPRECATION_ERROR") +internal fun JSONRPCNotification.fromJSON(): Notification = this.fromJSON() + +/** + * Represents the result of a request, including additional metadata. + */ +@Deprecated( + message = "Use `RequestResult` instead", + replaceWith = ReplaceWith("RequestResult", "io.modelcontextprotocol.kotlin.sdk.types.RequestResult"), + level = DeprecationLevel.ERROR, +) +public typealias RequestResult = io.modelcontextprotocol.kotlin.sdk.types.RequestResult + +/** + * An empty result for a request containing optional metadata. + */ +@Deprecated( + message = "Use `EmptyResult` instead", + replaceWith = ReplaceWith("EmptyResult", "io.modelcontextprotocol.kotlin.sdk.types.EmptyResult"), + level = DeprecationLevel.ERROR, +) +public typealias EmptyRequestResult = io.modelcontextprotocol.kotlin.sdk.types.EmptyResult + +/** + * A uniquely identifying ID for a request in JSON-RPC. + */ +@Deprecated( + message = "Use `RequestId` instead", + replaceWith = ReplaceWith("RequestId", "io.modelcontextprotocol.kotlin.sdk.types.RequestId"), + level = DeprecationLevel.ERROR, +) +public object RequestId { + @Deprecated( + message = "Use `RequestId.StringId` instead", + replaceWith = ReplaceWith("RequestId.StringId", "io.modelcontextprotocol.kotlin.sdk.types.RequestId.StringId"), + level = DeprecationLevel.ERROR, + ) + public fun StringId(value: String): io.modelcontextprotocol.kotlin.sdk.types.RequestId.StringId = + io.modelcontextprotocol.kotlin.sdk.types.RequestId.StringId(value) + + @Deprecated( + message = "Use `RequestId.NumberId` instead", + replaceWith = ReplaceWith("RequestId.NumberId", "io.modelcontextprotocol.kotlin.sdk.types.RequestId.NumberId"), + level = DeprecationLevel.ERROR, + ) + public fun NumberId(value: Long): io.modelcontextprotocol.kotlin.sdk.types.RequestId.NumberId = + io.modelcontextprotocol.kotlin.sdk.types.RequestId.NumberId(value) +} + +/** + * Represents a JSON-RPC message in the protocol. + */ +@Deprecated( + message = "Use `JSONRPCMessage` instead", + replaceWith = ReplaceWith("JSONRPCMessage", "io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage"), + level = DeprecationLevel.ERROR, +) +public typealias JSONRPCMessage = io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage + +/** + * A request that expects a response. + */ +@Deprecated( + message = "Use `JSONRPCRequest` instead", + replaceWith = ReplaceWith("JSONRPCRequest", "io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest"), + level = DeprecationLevel.ERROR, +) +public typealias JSONRPCRequest = io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest + +/** + * A notification which does not expect a response. + */ +@Deprecated( + message = "Use `JSONRPCNotification` instead", + replaceWith = ReplaceWith("JSONRPCNotification", "io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification"), + level = DeprecationLevel.ERROR, +) +public typealias JSONRPCNotification = io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification + +/** + * A successful (non-error) response to a request. + */ +@Deprecated( + message = "Use `JSONRPCResponse` instead", + replaceWith = ReplaceWith("JSONRPCResponse", "io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse"), + level = DeprecationLevel.ERROR, +) +public typealias JSONRPCResponse = io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse + +/** + * An incomplete set of error codes that may appear in JSON-RPC responses. + */ +@Deprecated( + message = "Use `RPCError` instead", + replaceWith = ReplaceWith("RPCError", "io.modelcontextprotocol.kotlin.sdk.types.RPCError"), + level = DeprecationLevel.ERROR, +) +public sealed interface ErrorCode { + public val code: Int + + public object Defined { + @Deprecated( + message = "Use `RPCError.ErrorCode.CONNECTION_CLOSED` instead", + replaceWith = ReplaceWith( + "RPCError.ErrorCode.CONNECTION_CLOSED", + "io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.CONNECTION_CLOSED", + ), + level = DeprecationLevel.ERROR, + ) + public val ConnectionClosed: Int = io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.CONNECTION_CLOSED + + @Deprecated( + message = "Use `RPCError.ErrorCode.REQUEST_TIMEOUT` instead", + replaceWith = ReplaceWith( + "RPCError.ErrorCode.REQUEST_TIMEOUT", + "io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.REQUEST_TIMEOUT", + ), + level = DeprecationLevel.ERROR, + ) + public val RequestTimeout: Int = io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.REQUEST_TIMEOUT + + @Deprecated( + message = "Use `RPCError.ErrorCode.PARSE_ERROR` instead", + replaceWith = ReplaceWith( + "RPCError.ErrorCode.PARSE_ERROR", + "io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.PARSE_ERROR", + ), + level = DeprecationLevel.ERROR, + ) + public val ParseError: Int = io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.PARSE_ERROR + + @Deprecated( + message = "Use `RPCError.ErrorCode.INVALID_REQUEST` instead", + replaceWith = ReplaceWith( + "RPCError.ErrorCode.INVALID_REQUEST", + "io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.INVALID_REQUEST", + ), + level = DeprecationLevel.ERROR, + ) + public val InvalidRequest: Int = io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.INVALID_REQUEST + + @Deprecated( + message = "Use `RPCError.ErrorCode.METHOD_NOT_FOUND` instead", + replaceWith = ReplaceWith( + "RPCError.ErrorCode.METHOD_NOT_FOUND", + "io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.METHOD_NOT_FOUND", + ), + level = DeprecationLevel.ERROR, + ) + public val MethodNotFound: Int = io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.METHOD_NOT_FOUND + + @Deprecated( + message = "Use `RPCError.ErrorCode.INVALID_PARAMS` instead", + replaceWith = ReplaceWith( + "RPCError.ErrorCode.INVALID_PARAMS", + "io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.INVALID_PARAMS", + ), + level = DeprecationLevel.ERROR, + ) + public val InvalidParams: Int = io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.INVALID_PARAMS + + @Deprecated( + message = "Use `RPCError.ErrorCode.INTERNAL_ERROR` instead", + replaceWith = ReplaceWith( + "RPCError.ErrorCode.INTERNAL_ERROR", + "io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.INTERNAL_ERROR", + ), + level = DeprecationLevel.ERROR, + ) + public val InternalError: Int = io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.INTERNAL_ERROR + } + + @Deprecated( + message = "Use `RPCError` instead", + replaceWith = ReplaceWith("RPCError", "io.modelcontextprotocol.kotlin.sdk.types.RPCError"), + level = DeprecationLevel.ERROR, + ) + @Serializable + @Suppress("DEPRECATION_ERROR") + public data class Unknown(override val code: Int) : ErrorCode +} + +/** + * A response to a request that indicates an error occurred. + */ +@Deprecated( + message = "Use `JSONRPCError` instead", + replaceWith = ReplaceWith("JSONRPCError", "io.modelcontextprotocol.kotlin.sdk.types.JSONRPCError"), + level = DeprecationLevel.ERROR, +) +public typealias JSONRPCError = io.modelcontextprotocol.kotlin.sdk.types.JSONRPCError + +/** + * Base interface for notification parameters with optional metadata. + */ +@Deprecated( + message = "Use `NotificationParams` instead", + replaceWith = ReplaceWith("NotificationParams", "io.modelcontextprotocol.kotlin.sdk.types.NotificationParams"), + level = DeprecationLevel.ERROR, +) +public typealias NotificationParams = io.modelcontextprotocol.kotlin.sdk.types.NotificationParams + +/* Cancellation */ + +@Deprecated( + message = "Use `CancelledNotification` instead", + replaceWith = ReplaceWith( + "CancelledNotification", + "io.modelcontextprotocol.kotlin.sdk.types.CancelledNotification", + ), + level = DeprecationLevel.ERROR, +) +public object CancelledNotification { + @Deprecated( + message = "Use `CancelledNotificationParams` instead", + replaceWith = ReplaceWith( + "CancelledNotificationParams", + "io.modelcontextprotocol.kotlin.sdk.types.CancelledNotificationParams", + ), + level = DeprecationLevel.ERROR, + ) + public fun Params( + requestId: io.modelcontextprotocol.kotlin.sdk.types.RequestId, + reason: String? = null, + meta: JsonObject? = null, + ): io.modelcontextprotocol.kotlin.sdk.types.CancelledNotificationParams = + io.modelcontextprotocol.kotlin.sdk.types.CancelledNotificationParams(requestId, reason, meta) +} + +/** + * 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. + */ +@Deprecated( + message = "Use `CancelledNotification` instead", + replaceWith = ReplaceWith( + "CancelledNotification", + "io.modelcontextprotocol.kotlin.sdk.types.CancelledNotification", + ), + level = DeprecationLevel.ERROR, +) +public fun CancelledNotification( + params: io.modelcontextprotocol.kotlin.sdk.types.CancelledNotificationParams, +): io.modelcontextprotocol.kotlin.sdk.types.CancelledNotification = + io.modelcontextprotocol.kotlin.sdk.types.CancelledNotification(params) + +/* Initialization */ + +/** + * Describes the name and version of an MCP implementation. + */ +@Deprecated( + message = "Use `Implementation` instead", + replaceWith = ReplaceWith("Implementation", "io.modelcontextprotocol.kotlin.sdk.types.Implementation"), + level = DeprecationLevel.ERROR, +) +public typealias Implementation = io.modelcontextprotocol.kotlin.sdk.types.Implementation + +@Deprecated( + message = "Use `ClientCapabilities` instead", + replaceWith = ReplaceWith("ClientCapabilities", "io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities"), + level = DeprecationLevel.ERROR, +) +public object ClientCapabilities { + /** + * Creates an instance of [Roots], representing the client's capability + * to list roots and optionally listen for changes in the roots list. + * + * @param listChanged Optional flag indicating whether the client supports notifications + * for changes to the roots list. If set to true, the client will notify the server when + * roots are added or removed. + * @return A new instance of [Roots] configured with the provided [listChanged] value. + */ + @Deprecated( + message = "Use `ClientCapabilities.Roots` instead", + replaceWith = ReplaceWith( + "ClientCapabilities.Roots", + "io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities.Roots", + ), + level = DeprecationLevel.ERROR, + ) + public fun Roots(listChanged: Boolean? = null): TypesRoots = TypesRoots(listChanged) +} + +/** + * 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. + */ +@Deprecated( + message = "Use `ClientCapabilities` instead", + replaceWith = ReplaceWith("ClientCapabilities", "io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities"), + level = DeprecationLevel.ERROR, +) +public fun ClientCapabilities( + sampling: JsonObject? = null, + roots: TypesRoots? = null, + elicitation: JsonObject? = null, + experimental: JsonObject? = null, +): io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities = + io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities(sampling, roots, elicitation, experimental) + +/** + * Represents a request sent by the client. + */ +@Deprecated( + message = "Use `ClientRequest` instead", + replaceWith = ReplaceWith("ClientRequest", "io.modelcontextprotocol.kotlin.sdk.types.ClientRequest"), + level = DeprecationLevel.ERROR, +) +public typealias ClientRequest = io.modelcontextprotocol.kotlin.sdk.types.ClientRequest + +/** + * Represents a notification sent by the client. + */ +@Deprecated( + message = "Use `ClientNotification` instead", + replaceWith = ReplaceWith("ClientNotification", "io.modelcontextprotocol.kotlin.sdk.types.ClientNotification"), + level = DeprecationLevel.ERROR, +) +public typealias ClientNotification = io.modelcontextprotocol.kotlin.sdk.types.ClientNotification + +/** + * Represents a result returned to the client. + */ +@Deprecated( + message = "Use `ClientResult` instead", + replaceWith = ReplaceWith("ClientResult", "io.modelcontextprotocol.kotlin.sdk.types.ClientResult"), + level = DeprecationLevel.ERROR, +) +public typealias ClientResult = io.modelcontextprotocol.kotlin.sdk.types.ClientResult + +/** + * Represents a request sent by the server. + */ +@Deprecated( + message = "Use `ServerRequest` instead", + replaceWith = ReplaceWith("ServerRequest", "io.modelcontextprotocol.kotlin.sdk.types.ServerRequest"), + level = DeprecationLevel.ERROR, +) +public typealias ServerRequest = io.modelcontextprotocol.kotlin.sdk.types.ServerRequest + +/** + * Represents a notification sent by the server. + */ +@Deprecated( + message = "Use `ServerNotification` instead", + replaceWith = ReplaceWith("ServerNotification", "io.modelcontextprotocol.kotlin.sdk.types.ServerNotification"), + level = DeprecationLevel.ERROR, +) +public typealias ServerNotification = io.modelcontextprotocol.kotlin.sdk.types.ServerNotification + +/** + * Represents a result returned by the server. + */ +@Deprecated( + message = "Use `ServerResult` instead", + replaceWith = ReplaceWith("ServerResult", "io.modelcontextprotocol.kotlin.sdk.types.ServerResult"), + level = DeprecationLevel.ERROR, +) +public typealias ServerResult = io.modelcontextprotocol.kotlin.sdk.types.ServerResult + +/** + * Represents a request or notification for an unknown method. + * + * @param method The method that is unknown. + */ +@Serializable +@Deprecated( + message = "This class will be removed", + level = DeprecationLevel.ERROR, +) +@Suppress("DEPRECATION_ERROR") +public data class UnknownMethodRequestOrNotification(val method: Method, val params: NotificationParams? = null) + +/** + * This request is sent from the client to the server when it first connects, asking it to begin initialization. + */ +@Deprecated( + message = "Use `InitializeRequest` instead", + replaceWith = ReplaceWith("InitializeRequest", "io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest"), + level = DeprecationLevel.ERROR, +) +public typealias InitializeRequest = io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest + +@Deprecated( + message = "Use `ServerCapabilities` instead", + replaceWith = ReplaceWith("ServerCapabilities", "io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities"), + level = DeprecationLevel.ERROR, +) +public object ServerCapabilities { + + @Deprecated( + message = "Use `ServerCapabilities.Prompts` instead", + replaceWith = ReplaceWith( + "ServerCapabilities.Prompts", + "io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities.Prompts", + ), + level = DeprecationLevel.ERROR, + ) + public fun Prompts( + listChanged: Boolean? = null, + ): io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities.Prompts = + io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities.Prompts(listChanged) + + @Deprecated( + message = "Use `ServerCapabilities.Resources` instead", + replaceWith = ReplaceWith( + "ServerCapabilities.Resources", + "io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities.Resources", + ), + level = DeprecationLevel.ERROR, + ) + public fun Resources( + listChanged: Boolean? = null, + subscribe: Boolean? = null, + ): io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities.Resources = + io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities.Resources(listChanged, subscribe) + + @Deprecated( + message = "Use `ServerCapabilities.Tools` instead", + replaceWith = ReplaceWith( + "ServerCapabilities.Tools", + "io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities.Tools", + ), + level = DeprecationLevel.ERROR, + ) + public fun Tools(listChanged: Boolean? = null): io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities.Tools = + io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities.Tools(listChanged) +} + +/** + * Represents the capabilities that a server can support. + * + * @property experimental Experimental, non-standard capabilities that the server supports. + * @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. + */ +@Deprecated( + message = "Use `ServerCapabilities` instead", + replaceWith = ReplaceWith("ServerCapabilities", "io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities"), + level = DeprecationLevel.ERROR, +) +@Suppress("LongParameterList") +public fun ServerCapabilities( + tools: io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities.Tools? = null, + resources: io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities.Resources? = null, + prompts: io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities.Prompts? = null, + logging: JsonObject? = null, + completions: JsonObject? = null, + experimental: JsonObject? = null, +): io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities = + io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities( + tools, + resources, + prompts, + logging, + completions, + experimental, + ) + +/** + * After receiving an initialized request from the client, the server sends this response. + */ +@Deprecated( + message = "Use `InitializeResult` instead", + replaceWith = ReplaceWith("InitializeResult", "io.modelcontextprotocol.kotlin.sdk.types.InitializeResult"), + level = DeprecationLevel.ERROR, +) +public typealias InitializeResult = io.modelcontextprotocol.kotlin.sdk.types.InitializeResult + +@Deprecated( + message = "Use `InitializedNotification` instead", + replaceWith = ReplaceWith( + "InitializedNotification", + "io.modelcontextprotocol.kotlin.sdk.types.InitializedNotification", + ), + level = DeprecationLevel.ERROR, +) +public object InitializedNotification { + @Deprecated( + message = "Use `BaseNotificationParams` instead", + replaceWith = ReplaceWith( + "BaseNotificationParams", + "io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams", + ), + level = DeprecationLevel.ERROR, + ) + @Suppress("DEPRECATION_ERROR") + public fun Params( + meta: JsonObject = EmptyJsonObject, + ): io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams = + io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams(meta) +} + +/** + * This notification is sent from the client to the server after initialization has finished. + */ +@Deprecated( + message = "Use `InitializedNotification` instead", + replaceWith = ReplaceWith( + "InitializedNotification", + "io.modelcontextprotocol.kotlin.sdk.types.InitializedNotification", + ), + level = DeprecationLevel.ERROR, +) +public fun InitializedNotification( + params: io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams? = null, +): io.modelcontextprotocol.kotlin.sdk.types.InitializedNotification = + io.modelcontextprotocol.kotlin.sdk.types.InitializedNotification(params) + +/* 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. + */ +@Deprecated( + message = "Use `PingRequest` instead", + replaceWith = ReplaceWith("PingRequest", "io.modelcontextprotocol.kotlin.sdk.types.PingRequest"), + level = DeprecationLevel.ERROR, +) +public typealias PingRequest = io.modelcontextprotocol.kotlin.sdk.types.PingRequest + +/** + * Represents the base interface for progress tracking. + */ +@Serializable +@Deprecated( + message = "This interface will be removed", + level = DeprecationLevel.ERROR, +) +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. + */ +@Deprecated( + message = "Use `Progress` instead", + replaceWith = ReplaceWith("Progress", "io.modelcontextprotocol.kotlin.sdk.types.Progress"), + level = DeprecationLevel.ERROR, +) +public typealias Progress = io.modelcontextprotocol.kotlin.sdk.types.Progress + +@Deprecated( + message = "Use `ProgressNotification` instead", + replaceWith = ReplaceWith("ProgressNotification", "io.modelcontextprotocol.kotlin.sdk.types.ProgressNotification"), + level = DeprecationLevel.ERROR, +) +public object ProgressNotification { + @Deprecated( + message = "Use `ProgressNotificationParams` instead", + replaceWith = ReplaceWith( + "ProgressNotificationParams", + "io.modelcontextprotocol.kotlin.sdk.types.ProgressNotificationParams", + ), + level = DeprecationLevel.ERROR, + ) + public fun Params( + progressToken: io.modelcontextprotocol.kotlin.sdk.types.ProgressToken, + progress: Double, + total: Double? = null, + message: String? = null, + meta: JsonObject? = null, + ): io.modelcontextprotocol.kotlin.sdk.types.ProgressNotificationParams = + io.modelcontextprotocol.kotlin.sdk.types.ProgressNotificationParams( + progressToken, + progress, + total, + message, + meta, + ) +} + +/** + * An out-of-band notification used to inform the receiver of a progress update for a long-running request. + */ +@Deprecated( + message = "Use `ProgressNotification` instead", + replaceWith = ReplaceWith("ProgressNotification", "io.modelcontextprotocol.kotlin.sdk.types.ProgressNotification"), + level = DeprecationLevel.ERROR, +) +public fun ProgressNotification( + params: io.modelcontextprotocol.kotlin.sdk.types.ProgressNotificationParams, +): io.modelcontextprotocol.kotlin.sdk.types.ProgressNotification = + io.modelcontextprotocol.kotlin.sdk.types.ProgressNotification(params) + +/* Pagination */ + +/** + * Represents a request supporting pagination. + */ +@Deprecated( + message = "Use `PaginatedRequest` instead", + replaceWith = ReplaceWith("PaginatedRequest", "io.modelcontextprotocol.kotlin.sdk.types.PaginatedRequest"), + level = DeprecationLevel.ERROR, +) +public typealias PaginatedRequest = io.modelcontextprotocol.kotlin.sdk.types.PaginatedRequest + +/** + * Represents a paginated result of a request. + */ +@Deprecated( + message = "Use `PaginatedResult` instead", + replaceWith = ReplaceWith("PaginatedResult", "io.modelcontextprotocol.kotlin.sdk.types.PaginatedResult"), + level = DeprecationLevel.ERROR, +) +public typealias PaginatedResult = io.modelcontextprotocol.kotlin.sdk.types.PaginatedResult + +/* Resources */ + +/** + * The contents of a specific resource or sub-resource. + */ +@Deprecated( + message = "Use `ResourceContents` instead", + replaceWith = ReplaceWith("ResourceContents", "io.modelcontextprotocol.kotlin.sdk.types.ResourceContents"), + level = DeprecationLevel.ERROR, +) +public typealias ResourceContents = io.modelcontextprotocol.kotlin.sdk.types.ResourceContents + +/** + * Represents the text contents of a resource. + */ +@Deprecated( + message = "Use `TextResourceContents` instead", + replaceWith = ReplaceWith("TextResourceContents", "io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents"), + level = DeprecationLevel.ERROR, +) +public typealias TextResourceContents = io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents + +/** + * Represents the binary contents of a resource encoded as a base64 string. + */ +@Deprecated( + message = "Use `BlobResourceContents` instead", + replaceWith = ReplaceWith("BlobResourceContents", "io.modelcontextprotocol.kotlin.sdk.types.BlobResourceContents"), + level = DeprecationLevel.ERROR, +) +public typealias BlobResourceContents = io.modelcontextprotocol.kotlin.sdk.types.BlobResourceContents + +/** + * Represents resource contents with unknown or unspecified data. + */ +@Deprecated( + message = "Use `UnknownResourceContents` instead", + replaceWith = ReplaceWith( + "UnknownResourceContents", + "io.modelcontextprotocol.kotlin.sdk.types.UnknownResourceContents", + ), + level = DeprecationLevel.ERROR, +) +public typealias UnknownResourceContents = io.modelcontextprotocol.kotlin.sdk.types.UnknownResourceContents + +/** + * A known resource that the server is capable of reading. + */ +@Deprecated( + message = "Use `Resource` instead", + replaceWith = ReplaceWith("Resource", "io.modelcontextprotocol.kotlin.sdk.types.Resource"), + level = DeprecationLevel.ERROR, +) +public typealias Resource = io.modelcontextprotocol.kotlin.sdk.types.Resource + +/** + * A template description for resources available on the server. + */ +@Deprecated( + message = "Use `ResourceTemplate` instead", + replaceWith = ReplaceWith("ResourceTemplate", "io.modelcontextprotocol.kotlin.sdk.types.ResourceTemplate"), + level = DeprecationLevel.ERROR, +) +public typealias ResourceTemplate = io.modelcontextprotocol.kotlin.sdk.types.ResourceTemplate + +/** + * Sent from the client to request a list of resources the server has. + */ +@Deprecated( + message = "Use `ListResourcesRequest` instead", + replaceWith = ReplaceWith("ListResourcesRequest", "io.modelcontextprotocol.kotlin.sdk.types.ListResourcesRequest"), + level = DeprecationLevel.ERROR, +) +public typealias ListResourcesRequest = io.modelcontextprotocol.kotlin.sdk.types.ListResourcesRequest + +/** + * The server's response to a resources/list request from the client. + */ +@Deprecated( + message = "Use `ListResourcesResult` instead", + replaceWith = ReplaceWith("ListResourcesResult", "io.modelcontextprotocol.kotlin.sdk.types.ListResourcesResult"), + level = DeprecationLevel.ERROR, +) +public typealias ListResourcesResult = io.modelcontextprotocol.kotlin.sdk.types.ListResourcesResult + +/** + * Sent from the client to request a list of resource templates the server has. + */ +@Deprecated( + message = "Use `ListResourceTemplatesRequest` instead", + replaceWith = ReplaceWith( + "ListResourceTemplatesRequest", + "io.modelcontextprotocol.kotlin.sdk.types.ListResourceTemplatesRequest", + ), + level = DeprecationLevel.ERROR, +) +public typealias ListResourceTemplatesRequest = io.modelcontextprotocol.kotlin.sdk.types.ListResourceTemplatesRequest + +/** + * The server's response to a resources/templates/list request from the client. + */ +@Deprecated( + message = "Use `ListResourceTemplatesResult` instead", + replaceWith = ReplaceWith( + "ListResourceTemplatesResult", + "io.modelcontextprotocol.kotlin.sdk.types.ListResourceTemplatesResult", + ), + level = DeprecationLevel.ERROR, +) +public typealias ListResourceTemplatesResult = io.modelcontextprotocol.kotlin.sdk.types.ListResourceTemplatesResult + +/** + * Sent from the client to the server to read a specific resource URI. + */ +@Deprecated( + message = "Use `ReadResourceRequest` instead", + replaceWith = ReplaceWith("ReadResourceRequest", "io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest"), + level = DeprecationLevel.ERROR, +) +public typealias ReadResourceRequest = io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest + +/** + * The server's response to a resources/read request from the client. + */ +@Deprecated( + message = "Use `ReadResourceResult` instead", + replaceWith = ReplaceWith("ReadResourceResult", "io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult"), + level = DeprecationLevel.ERROR, +) +public typealias ReadResourceResult = io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult + +@Deprecated( + message = "Use `ResourceListChangedNotification` instead", + replaceWith = ReplaceWith( + "ResourceListChangedNotification", + "io.modelcontextprotocol.kotlin.sdk.types.ResourceListChangedNotification", + ), + level = DeprecationLevel.ERROR, +) +public object ResourceListChangedNotification { + @Deprecated( + message = "Use `BaseNotificationParams` instead", + replaceWith = ReplaceWith( + "BaseNotificationParams", + "io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams", + ), + level = DeprecationLevel.ERROR, + ) + public fun Params(meta: JsonObject? = null): io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams = + io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams(meta) +} + +/** + * 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. + */ +@Deprecated( + message = "Use `ResourceListChangedNotification` instead", + replaceWith = ReplaceWith( + "ResourceListChangedNotification", + "io.modelcontextprotocol.kotlin.sdk.types.ResourceListChangedNotification", + ), + level = DeprecationLevel.ERROR, +) +public fun ResourceListChangedNotification( + params: io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams? = null, +): io.modelcontextprotocol.kotlin.sdk.types.ResourceListChangedNotification = + io.modelcontextprotocol.kotlin.sdk.types.ResourceListChangedNotification(params) + +/** + * Sent from the client to request resources/updated notifications from the server + * whenever a particular resource changes. + */ +@Deprecated( + message = "Use `SubscribeRequest` instead", + replaceWith = ReplaceWith("SubscribeRequest", "io.modelcontextprotocol.kotlin.sdk.types.SubscribeRequest"), + level = DeprecationLevel.ERROR, +) +public typealias SubscribeRequest = io.modelcontextprotocol.kotlin.sdk.types.SubscribeRequest + +/** + * Sent from the client to request cancellation of resources/updated notifications from the server. + * This should follow a previous resources/subscribe request. + */ +@Deprecated( + message = "Use `UnsubscribeRequest` instead", + replaceWith = ReplaceWith("UnsubscribeRequest", "io.modelcontextprotocol.kotlin.sdk.types.UnsubscribeRequest"), + level = DeprecationLevel.ERROR, +) +public typealias UnsubscribeRequest = io.modelcontextprotocol.kotlin.sdk.types.UnsubscribeRequest + +@Deprecated( + message = "Use `ResourceUpdatedNotification` instead", + replaceWith = ReplaceWith( + "ResourceUpdatedNotification", + "io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotification", + ), + level = DeprecationLevel.ERROR, +) +public object ResourceUpdatedNotification { + @Deprecated( + message = "Use `ResourceUpdatedNotificationParams` instead", + replaceWith = ReplaceWith( + "ResourceUpdatedNotificationParams", + "io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotificationParams", + ), + level = DeprecationLevel.ERROR, + ) + public fun Params( + uri: String, + meta: JsonObject? = null, + ): io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotificationParams = + io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotificationParams(uri, meta) +} + +/** + * 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. + */ +@Deprecated( + message = "Use `ResourceUpdatedNotification` instead", + replaceWith = ReplaceWith( + "ResourceUpdatedNotification", + "io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotification", + ), + level = DeprecationLevel.ERROR, +) +public fun ResourceUpdatedNotification( + params: io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotificationParams, +): io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotification = + io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotification(params) + +/* Prompts */ + +/** + * Describes an argument that a prompt can accept. + */ +@Deprecated( + message = "Use `PromptArgument` instead", + replaceWith = ReplaceWith("PromptArgument", "io.modelcontextprotocol.kotlin.sdk.types.PromptArgument"), + level = DeprecationLevel.ERROR, +) +public typealias PromptArgument = io.modelcontextprotocol.kotlin.sdk.types.PromptArgument + +/** + * A prompt or prompt template that the server offers. + */ +@Deprecated( + message = "Use `Prompt` instead", + replaceWith = ReplaceWith("Prompt", "io.modelcontextprotocol.kotlin.sdk.types.Prompt"), + level = DeprecationLevel.ERROR, +) +public typealias Prompt = io.modelcontextprotocol.kotlin.sdk.types.Prompt + +/** + * Sent from the client to request a list of prompts and prompt templates the server has. + */ +@Deprecated( + message = "Use `ListPromptsRequest` instead", + replaceWith = ReplaceWith("ListPromptsRequest", "io.modelcontextprotocol.kotlin.sdk.types.ListPromptsRequest"), + level = DeprecationLevel.ERROR, +) +public typealias ListPromptsRequest = io.modelcontextprotocol.kotlin.sdk.types.ListPromptsRequest + +/** + * The server's response to a prompts/list request from the client. + */ +@Deprecated( + message = "Use `ListPromptsResult` instead", + replaceWith = ReplaceWith("ListPromptsResult", "io.modelcontextprotocol.kotlin.sdk.types.ListPromptsResult"), + level = DeprecationLevel.ERROR, +) +public typealias ListPromptsResult = io.modelcontextprotocol.kotlin.sdk.types.ListPromptsResult + +/** + * Used by the client to get a prompt provided by the server. + */ +@Deprecated( + message = "Use `GetPromptRequest` instead", + replaceWith = ReplaceWith("GetPromptRequest", "io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest"), + level = DeprecationLevel.ERROR, +) +public typealias GetPromptRequest = io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest + +/** + * Represents the content of a prompt message. + */ +@Deprecated( + message = "Use `ContentBlock` instead", + replaceWith = ReplaceWith("ContentBlock"), + level = DeprecationLevel.ERROR, +) +public typealias PromptMessageContent = ContentBlock + +/** + * Represents prompt message content that is either text, image or audio. + */ +@Deprecated( + message = "Use `MediaContent` instead", + replaceWith = ReplaceWith("MediaContent"), + level = DeprecationLevel.ERROR, +) +public typealias PromptMessageContentMultimodal = io.modelcontextprotocol.kotlin.sdk.types.MediaContent + +/** + * Text provided to or from an LLM. + */ +@Deprecated( + message = "Use `TextContent` instead", + replaceWith = ReplaceWith("TextContent", "io.modelcontextprotocol.kotlin.sdk.types.TextContent"), + level = DeprecationLevel.ERROR, +) +public typealias TextContent = io.modelcontextprotocol.kotlin.sdk.types.TextContent + +/** + * An image provided to or from an LLM. + */ +@Deprecated( + message = "Use `ImageContent` instead", + replaceWith = ReplaceWith("ImageContent", "io.modelcontextprotocol.kotlin.sdk.types.ImageContent"), + level = DeprecationLevel.ERROR, +) +public typealias ImageContent = io.modelcontextprotocol.kotlin.sdk.types.ImageContent + +/** + * Audio provided to or from an LLM. + */ +@Deprecated( + message = "Use `AudioContent` instead", + replaceWith = ReplaceWith("AudioContent", "io.modelcontextprotocol.kotlin.sdk.types.AudioContent"), + level = DeprecationLevel.ERROR, +) +public typealias AudioContent = io.modelcontextprotocol.kotlin.sdk.types.AudioContent + +/** + * Unknown content provided to or from an LLM. + */ +@Serializable +@Deprecated( + message = "This class will be removed.", + replaceWith = ReplaceWith("MediaContent"), + level = DeprecationLevel.ERROR, +) +public data class UnknownContent(val type: String) + +/** + * The contents of a resource, embedded into a prompt or tool call result. + */ +@Deprecated( + message = "Use `EmbeddedResource` instead", + replaceWith = ReplaceWith("EmbeddedResource", "io.modelcontextprotocol.kotlin.sdk.types.EmbeddedResource"), + level = DeprecationLevel.ERROR, +) +public typealias EmbeddedResource = io.modelcontextprotocol.kotlin.sdk.types.EmbeddedResource + +/** + * Enum representing the role of a participant. + */ +@Deprecated( + message = "Use `Role` instead", + replaceWith = ReplaceWith("Role", "io.modelcontextprotocol.kotlin.sdk.types.Role"), + level = DeprecationLevel.ERROR, +) +public object Role { + @Deprecated( + message = "Use `Role.User` instead", + replaceWith = ReplaceWith("Role.User", "io.modelcontextprotocol.kotlin.sdk.types.Role"), + level = DeprecationLevel.ERROR, + ) + public val user: io.modelcontextprotocol.kotlin.sdk.types.Role = io.modelcontextprotocol.kotlin.sdk.types.Role.User + + @Deprecated( + message = "Use `Role.Assistant` instead", + replaceWith = ReplaceWith("Role.Assistant", "io.modelcontextprotocol.kotlin.sdk.types.Role"), + level = DeprecationLevel.ERROR, + ) + public val assistant: io.modelcontextprotocol.kotlin.sdk.types.Role = + io.modelcontextprotocol.kotlin.sdk.types.Role.Assistant +} + +/** + * Optional annotations for the client. + * The client can use annotations to inform how objects are used or displayed. + */ +@Deprecated( + message = "Use `Annotations` instead", + replaceWith = ReplaceWith("Annotations", "io.modelcontextprotocol.kotlin.sdk.types.Annotations"), + level = DeprecationLevel.ERROR, +) +public typealias Annotations = io.modelcontextprotocol.kotlin.sdk.types.Annotations + +/** + * Describes a message returned as part of a prompt. + */ +@Deprecated( + message = "Use `PromptMessage` instead", + replaceWith = ReplaceWith("PromptMessage", "io.modelcontextprotocol.kotlin.sdk.types.PromptMessage"), + level = DeprecationLevel.ERROR, +) +public typealias PromptMessage = io.modelcontextprotocol.kotlin.sdk.types.PromptMessage + +/** + * The server's response to a prompts/get request from the client. + */ +@Deprecated( + message = "Use `GetPromptResult` instead", + replaceWith = ReplaceWith("GetPromptResult", "io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult"), + level = DeprecationLevel.ERROR, +) +public typealias GetPromptResult = io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult + +@Deprecated( + message = "Use `PromptListChangedNotification` instead", + replaceWith = ReplaceWith( + "PromptListChangedNotification", + "io.modelcontextprotocol.kotlin.sdk.types.PromptListChangedNotification", + ), + level = DeprecationLevel.ERROR, +) +public object PromptListChangedNotification { + @Deprecated( + message = "Use `BaseNotificationParams` instead", + replaceWith = ReplaceWith( + "BaseNotificationParams", + "io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams", + ), + level = DeprecationLevel.ERROR, + ) + public fun Params(meta: JsonObject? = null): io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams = + io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams(meta) +} + +/** + * 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. + */ +@Deprecated( + message = "Use `PromptListChangedNotification` instead", + replaceWith = ReplaceWith( + "PromptListChangedNotification", + "io.modelcontextprotocol.kotlin.sdk.types.PromptListChangedNotification", + ), + level = DeprecationLevel.ERROR, +) +public fun PromptListChangedNotification( + params: io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams, +): io.modelcontextprotocol.kotlin.sdk.types.PromptListChangedNotification = + io.modelcontextprotocol.kotlin.sdk.types.PromptListChangedNotification(params) + +/* 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. + */ +@Deprecated( + message = "Use `ToolAnnotations` instead", + replaceWith = ReplaceWith("ToolAnnotations", "io.modelcontextprotocol.kotlin.sdk.types.ToolAnnotations"), + level = DeprecationLevel.ERROR, +) +public typealias ToolAnnotations = io.modelcontextprotocol.kotlin.sdk.types.ToolAnnotations + +@Deprecated( + message = "Use `Tool` instead", + replaceWith = ReplaceWith("Tool", "io.modelcontextprotocol.kotlin.sdk.types.Tool"), + level = DeprecationLevel.ERROR, +) +public object Tool { + @Deprecated( + message = "Use `ToolSchema` instead", + replaceWith = ReplaceWith("ToolSchema", "io.modelcontextprotocol.kotlin.sdk.types.ToolSchema"), + level = DeprecationLevel.ERROR, + ) + public fun Input( + properties: JsonObject? = null, + required: List? = null, + ): io.modelcontextprotocol.kotlin.sdk.types.ToolSchema = + io.modelcontextprotocol.kotlin.sdk.types.ToolSchema(properties, required) + + @Deprecated( + message = "Use `ToolSchema` instead", + replaceWith = ReplaceWith("ToolSchema", "io.modelcontextprotocol.kotlin.sdk.types.ToolSchema"), + level = DeprecationLevel.ERROR, + ) + public fun Output( + properties: JsonObject? = null, + required: List? = null, + ): io.modelcontextprotocol.kotlin.sdk.types.ToolSchema = + io.modelcontextprotocol.kotlin.sdk.types.ToolSchema(properties, required) +} + +/** + * Definition for a tool the client can call. + */ +@Deprecated( + message = "Use `Tool` instead", + replaceWith = ReplaceWith("Tool", "io.modelcontextprotocol.kotlin.sdk.types.Tool"), + level = DeprecationLevel.ERROR, +) +@Suppress("LongParameterList", "FunctionParameterNaming") +public fun Tool( + name: String, + inputSchema: io.modelcontextprotocol.kotlin.sdk.types.ToolSchema, + description: String? = null, + outputSchema: io.modelcontextprotocol.kotlin.sdk.types.ToolSchema? = null, + title: String? = null, + annotations: io.modelcontextprotocol.kotlin.sdk.types.ToolAnnotations? = null, + icons: List? = null, + _meta: JsonObject? = null, +): io.modelcontextprotocol.kotlin.sdk.types.Tool = io.modelcontextprotocol.kotlin.sdk.types.Tool( + name, + inputSchema, + description, + outputSchema, + title, + annotations, + icons, + _meta, +) + +/** + * Sent from the client to request a list of tools the server has. + */ +@Deprecated( + message = "Use `ListToolsRequest` instead", + replaceWith = ReplaceWith("ListToolsRequest", "io.modelcontextprotocol.kotlin.sdk.types.ListToolsRequest"), + level = DeprecationLevel.ERROR, +) +public typealias ListToolsRequest = io.modelcontextprotocol.kotlin.sdk.types.ListToolsRequest + +/** + * The server's response to a tools/list request from the client. + */ +@Deprecated( + message = "Use `ListToolsResult` instead", + replaceWith = ReplaceWith("ListToolsResult", "io.modelcontextprotocol.kotlin.sdk.types.ListToolsResult"), + level = DeprecationLevel.ERROR, +) +public typealias ListToolsResult = io.modelcontextprotocol.kotlin.sdk.types.ListToolsResult + +/** + * The server's response to a tool call. + */ +@Serializable +@Deprecated( + message = "This interface will be removed.", + replaceWith = ReplaceWith("CallToolResult", "io.modelcontextprotocol.kotlin.sdk.types.CallToolResult"), + level = DeprecationLevel.ERROR, +) +@Suppress("DEPRECATION_ERROR") +public sealed interface CallToolResultBase { + public val content: List + public val structuredContent: JsonObject? + public val isError: Boolean? get() = false +} + +@Deprecated( + message = "Use `CallToolResult` instead", + replaceWith = ReplaceWith("CallToolResult", "io.modelcontextprotocol.kotlin.sdk.types.CallToolResult"), + level = DeprecationLevel.ERROR, +) +public object CallToolResult + +/** + * The server's response to a tool call. + */ +@Deprecated( + message = "Use `CallToolResult` instead", + replaceWith = ReplaceWith("CallToolResult", "io.modelcontextprotocol.kotlin.sdk.types.CallToolResult"), + level = DeprecationLevel.ERROR, +) +public fun CallToolResult( + content: List, + isError: Boolean? = null, + structuredContent: JsonObject? = null, + meta: JsonObject? = null, +): io.modelcontextprotocol.kotlin.sdk.types.CallToolResult = + io.modelcontextprotocol.kotlin.sdk.types.CallToolResult(content, isError, structuredContent, meta) + +/** + * [CallToolResult] extended with backwards compatibility to protocol version 2024-11-05. + */ +@Serializable +@Deprecated( + message = "This class will be removed.", + replaceWith = ReplaceWith("ToolCallResult"), + level = DeprecationLevel.ERROR, +) +@Suppress("DEPRECATION_ERROR", "ConstructorParameterNaming") +public class CompatibilityCallToolResult( + public val content: List, + public val structuredContent: JsonObject? = null, + public val isError: Boolean? = false, + public val _meta: JsonObject = EmptyJsonObject, + public val toolResult: JsonObject = EmptyJsonObject, +) + +/** + * Used by the client to invoke a tool provided by the server. + */ +@Deprecated( + message = "Use `CallToolRequest` instead", + replaceWith = ReplaceWith("CallToolRequest", "io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest"), + level = DeprecationLevel.ERROR, +) +public typealias CallToolRequest = io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest + +@Deprecated( + message = "Use `ToolListChangedNotification` instead", + replaceWith = ReplaceWith( + "ToolListChangedNotification", + "io.modelcontextprotocol.kotlin.sdk.types.ToolListChangedNotification", + ), + level = DeprecationLevel.ERROR, +) +public object ToolListChangedNotification { + @Deprecated( + message = "Use `BaseNotificationParams` instead", + replaceWith = ReplaceWith( + "BaseNotificationParams", + "io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams", + ), + level = DeprecationLevel.ERROR, + ) + public fun Params(meta: JsonObject? = null): io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams = + io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams(meta) +} + +/** + * 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. + */ +@Deprecated( + message = "Use `ToolListChangedNotification` instead", + replaceWith = ReplaceWith( + "ToolListChangedNotification", + "io.modelcontextprotocol.kotlin.sdk.types.ToolListChangedNotification", + ), + level = DeprecationLevel.ERROR, +) +public fun ToolListChangedNotification( + params: io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams, +): io.modelcontextprotocol.kotlin.sdk.types.ToolListChangedNotification = + io.modelcontextprotocol.kotlin.sdk.types.ToolListChangedNotification(params) + +/* Logging */ + +/** + * The severity of a log message. + */ +@Deprecated( + message = "Use `LoggingLevel` instead", + replaceWith = ReplaceWith("LoggingLevel", "io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel"), + level = DeprecationLevel.ERROR, +) +public object LoggingLevel { + @Deprecated( + message = "Use `LoggingLevel.Debug` instead", + replaceWith = ReplaceWith("LoggingLevel.Debug", "io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel"), + level = DeprecationLevel.ERROR, + ) + public val debug: io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel = + io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel.Debug + + @Deprecated( + message = "Use `LoggingLevel.Info` instead", + replaceWith = ReplaceWith("LoggingLevel.Info", "io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel"), + level = DeprecationLevel.ERROR, + ) + public val info: io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel = + io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel.Info + + @Deprecated( + message = "Use `LoggingLevel.Notice` instead", + replaceWith = ReplaceWith("LoggingLevel.Notice", "io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel"), + level = DeprecationLevel.ERROR, + ) + public val notice: io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel = + io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel.Notice + + @Deprecated( + message = "Use `LoggingLevel.Warning` instead", + replaceWith = ReplaceWith("LoggingLevel.Warning", "io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel"), + level = DeprecationLevel.ERROR, + ) + public val warning: io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel = + io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel.Warning + + @Deprecated( + message = "Use `LoggingLevel.Error` instead", + replaceWith = ReplaceWith("LoggingLevel.Error", "io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel"), + level = DeprecationLevel.ERROR, + ) + public val error: io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel = + io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel.Error + + @Deprecated( + message = "Use `LoggingLevel.Critical` instead", + replaceWith = ReplaceWith("LoggingLevel.Critical", "io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel"), + level = DeprecationLevel.ERROR, + ) + public val critical: io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel = + io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel.Critical + + @Deprecated( + message = "Use `LoggingLevel.Alert` instead", + replaceWith = ReplaceWith("LoggingLevel.Alert", "io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel"), + level = DeprecationLevel.ERROR, + ) + public val alert: io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel = + io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel.Alert + + @Deprecated( + message = "Use `LoggingLevel.Emergency` instead", + replaceWith = ReplaceWith("LoggingLevel.Emergency", "io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel"), + level = DeprecationLevel.ERROR, + ) + public val emergency: io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel = + io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel.Emergency +} + +@Deprecated( + message = "Use `LoggingMessageNotification` instead", + replaceWith = ReplaceWith( + "LoggingMessageNotification", + "io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification", + ), + level = DeprecationLevel.ERROR, +) +public object LoggingMessageNotification { + @Deprecated( + message = "Use `LoggingMessageNotificationParams` instead", + replaceWith = ReplaceWith( + "LoggingMessageNotificationParams", + "io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotificationParams", + ), + level = DeprecationLevel.ERROR, + ) + public fun Params( + level: io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel, + data: JsonElement, + logger: String? = null, + meta: JsonObject? = null, + ): io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotificationParams = + io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotificationParams(level, data, logger, meta) + + @Deprecated( + message = "Use `SetLevelRequest` instead", + replaceWith = ReplaceWith( + "SetLevelRequest", + "io.modelcontextprotocol.kotlin.sdk.types.SetLevelRequest", + ), + level = DeprecationLevel.ERROR, + ) + public fun SetLevelRequest( + params: io.modelcontextprotocol.kotlin.sdk.types.SetLevelRequestParams, + ): io.modelcontextprotocol.kotlin.sdk.types.SetLevelRequest = + io.modelcontextprotocol.kotlin.sdk.types.SetLevelRequest(params) +} + +/** + * 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. + */ +@Deprecated( + message = "Use `LoggingMessageNotification` instead", + replaceWith = ReplaceWith( + "LoggingMessageNotification", + "io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification", + ), + level = DeprecationLevel.ERROR, +) +public fun LoggingMessageNotification( + params: io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotificationParams, +): io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification = + io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification(params) + +/* Sampling */ + +/** + * Hints to use for model selection. + */ +@Deprecated( + message = "Use `ModelHint` instead", + replaceWith = ReplaceWith("ModelHint", "io.modelcontextprotocol.kotlin.sdk.types.ModelHint"), + level = DeprecationLevel.ERROR, +) +public typealias ModelHint = io.modelcontextprotocol.kotlin.sdk.types.ModelHint + +/** + * The server's preferences for model selection, requested by the client during sampling. + */ +@Deprecated( + message = "Use `ModelPreferences` instead", + replaceWith = ReplaceWith("ModelPreferences", "io.modelcontextprotocol.kotlin.sdk.types.ModelPreferences"), + level = DeprecationLevel.ERROR, +) +public typealias ModelPreferences = io.modelcontextprotocol.kotlin.sdk.types.ModelPreferences + +/** + * Describes a message issued to or received from an LLM API. + */ +@Deprecated( + message = "Use `SamplingMessage` instead", + replaceWith = ReplaceWith("SamplingMessage", "io.modelcontextprotocol.kotlin.sdk.types.SamplingMessage"), + level = DeprecationLevel.ERROR, +) +public typealias SamplingMessage = io.modelcontextprotocol.kotlin.sdk.types.SamplingMessage + +@Deprecated( + message = "Use `CreateMessageRequest` instead", + replaceWith = ReplaceWith("CreateMessageRequest", "io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest"), + level = DeprecationLevel.ERROR, +) +public object CreateMessageRequest { + public val IncludeContext: io.modelcontextprotocol.kotlin.sdk.types.IncludeContext.Companion = + io.modelcontextprotocol.kotlin.sdk.types.IncludeContext +} + +/** + * 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. + */ +@Deprecated( + message = "Use `CreateMessageRequest` instead", + replaceWith = ReplaceWith("CreateMessageRequest", "io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest"), + level = DeprecationLevel.ERROR, +) +public fun CreateMessageRequest( + params: io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequestParams, +): io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest = + io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest(params) + +@Deprecated( + message = "Use `StopReason` instead", + replaceWith = ReplaceWith("StopReason", "io.modelcontextprotocol.kotlin.sdk.types.StopReason"), + level = DeprecationLevel.ERROR, +) +public typealias StopReason = io.modelcontextprotocol.kotlin.sdk.types.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. + */ +@Deprecated( + message = "Use `CreateMessageResult` instead", + replaceWith = ReplaceWith("CreateMessageResult", "io.modelcontextprotocol.kotlin.sdk.types.CreateMessageResult"), + level = DeprecationLevel.ERROR, +) +public typealias CreateMessageResult = io.modelcontextprotocol.kotlin.sdk.types.CreateMessageResult + +/* Autocomplete */ +@Deprecated( + message = "Use `Reference` instead", + replaceWith = ReplaceWith("Reference", "io.modelcontextprotocol.kotlin.sdk.types.Reference"), + level = DeprecationLevel.ERROR, +) +public typealias Reference = io.modelcontextprotocol.kotlin.sdk.types.Reference + +/** + * A reference to a resource or resource template definition. + */ +@Deprecated( + message = "Use `ResourceTemplateReference` instead", + replaceWith = ReplaceWith( + "ResourceTemplateReference", + "io.modelcontextprotocol.kotlin.sdk.types.ResourceTemplateReference", + ), + level = DeprecationLevel.ERROR, +) +public typealias ResourceTemplateReference = io.modelcontextprotocol.kotlin.sdk.types.ResourceTemplateReference + +/** + * Identifies a prompt. + */ +@Deprecated( + message = "Use `PromptReference` instead", + replaceWith = ReplaceWith("PromptReference", "io.modelcontextprotocol.kotlin.sdk.types.PromptReference"), + level = DeprecationLevel.ERROR, +) +public typealias PromptReference = io.modelcontextprotocol.kotlin.sdk.types.PromptReference + +/** + * Identifies a prompt. + */ +@Serializable +@Deprecated( + message = "This class will be removed.", + level = DeprecationLevel.ERROR, +) +public data class UnknownReference(val type: String) + +@Deprecated( + message = "Use `CompleteRequest` instead", + replaceWith = ReplaceWith("CompleteRequest", "io.modelcontextprotocol.kotlin.sdk.types.CompleteRequest"), + level = DeprecationLevel.ERROR, +) +public object CompleteRequest { + @Deprecated( + message = "Use `CompleteRequestParams.Argument` instead", + replaceWith = ReplaceWith( + "CompleteRequestParams.Argument", + "io.modelcontextprotocol.kotlin.sdk.types.CompleteRequestParams.Argument", + ), + level = DeprecationLevel.ERROR, + ) + public fun Argument( + name: String, + value: String, + ): io.modelcontextprotocol.kotlin.sdk.types.CompleteRequestParams.Argument = + io.modelcontextprotocol.kotlin.sdk.types.CompleteRequestParams.Argument(name, value) +} + +/** + * A request from the client to the server to ask for completion options. + */ +@Deprecated( + message = "Use `CompleteRequest` instead", + replaceWith = ReplaceWith("CompleteRequest", "io.modelcontextprotocol.kotlin.sdk.types.CompleteRequest"), + level = DeprecationLevel.ERROR, +) +public fun CompleteRequest( + params: io.modelcontextprotocol.kotlin.sdk.types.CompleteRequestParams, +): io.modelcontextprotocol.kotlin.sdk.types.CompleteRequest = + io.modelcontextprotocol.kotlin.sdk.types.CompleteRequest(params) + +@Deprecated( + message = "Use `CompleteResult` instead", + replaceWith = ReplaceWith("CompleteResult", "io.modelcontextprotocol.kotlin.sdk.types.CompleteResult"), + level = DeprecationLevel.ERROR, +) +public object CompleteResult { + @Deprecated( + message = "Use `CompleteResult.Completion` instead", + replaceWith = ReplaceWith( + "CompleteResult.Completion", + "io.modelcontextprotocol.kotlin.sdk.types.CompleteResult.Completion", + ), + level = DeprecationLevel.ERROR, + ) + public fun Completion( + values: List, + total: Int? = null, + hasMore: Boolean? = null, + ): io.modelcontextprotocol.kotlin.sdk.types.CompleteResult.Completion = + io.modelcontextprotocol.kotlin.sdk.types.CompleteResult.Completion(values, total, hasMore) +} + +/** + * The server's response to a completion/complete request + */ +@Deprecated( + message = "Use `CompleteResult` instead", + replaceWith = ReplaceWith("CompleteResult", "io.modelcontextprotocol.kotlin.sdk.types.CompleteResult"), + level = DeprecationLevel.ERROR, +) +public fun CompleteResult( + completion: io.modelcontextprotocol.kotlin.sdk.types.CompleteResult.Completion, + meta: JsonObject? = null, +): io.modelcontextprotocol.kotlin.sdk.types.CompleteResult = + io.modelcontextprotocol.kotlin.sdk.types.CompleteResult(completion, meta) + +/* Roots */ + +/** + * Represents a root directory or file that the server can operate on. + */ +@Deprecated( + message = "Use `Root` instead", + replaceWith = ReplaceWith("Root", "io.modelcontextprotocol.kotlin.sdk.types.Root"), + level = DeprecationLevel.ERROR, +) +public typealias Root = io.modelcontextprotocol.kotlin.sdk.types.Root + +/** + * Sent from the server to request a list of root URIs from the client. + */ +@Deprecated( + message = "Use `ListRootsRequest` instead", + replaceWith = ReplaceWith("ListRootsRequest", "io.modelcontextprotocol.kotlin.sdk.types.ListRootsRequest"), + level = DeprecationLevel.ERROR, +) +public typealias ListRootsRequest = io.modelcontextprotocol.kotlin.sdk.types.ListRootsRequest + +/** + * The client's response to a roots/list request from the server. + */ +@Deprecated( + message = "Use `ListRootsResult` instead", + replaceWith = ReplaceWith("ListRootsResult", "io.modelcontextprotocol.kotlin.sdk.types.ListRootsResult"), + level = DeprecationLevel.ERROR, +) +public typealias ListRootsResult = io.modelcontextprotocol.kotlin.sdk.types.ListRootsResult + +@Deprecated( + message = "Use `RootsListChangedNotification` instead", + replaceWith = ReplaceWith( + "RootsListChangedNotification", + "io.modelcontextprotocol.kotlin.sdk.types.RootsListChangedNotification", + ), + level = DeprecationLevel.ERROR, +) +public object RootsListChangedNotification { + @Deprecated( + message = "Use `BaseNotificationParams` instead", + replaceWith = ReplaceWith( + "BaseNotificationParams", + "io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams", + ), + level = DeprecationLevel.ERROR, + ) + public fun Params(meta: JsonObject? = null): io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams = + io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams(meta) +} + +/** + * A notification from the client to the server, informing it that the list of roots has changed. + */ +@Deprecated( + message = "Use `RootsListChangedNotification` instead", + replaceWith = ReplaceWith( + "RootsListChangedNotification", + "io.modelcontextprotocol.kotlin.sdk.types.RootsListChangedNotification", + ), + level = DeprecationLevel.ERROR, +) +public fun RootsListChangedNotification( + params: io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams? = null, +): io.modelcontextprotocol.kotlin.sdk.types.RootsListChangedNotification = + io.modelcontextprotocol.kotlin.sdk.types.RootsListChangedNotification(params) + +@Deprecated( + message = "Use `ElicitRequest` instead", + replaceWith = ReplaceWith( + "ElicitRequest", + "io.modelcontextprotocol.kotlin.sdk.types.ElicitRequest", + ), + level = DeprecationLevel.ERROR, +) +public object CreateElicitationRequest { + public fun RequestedSchema( + properties: JsonObject, + required: List? = null, + ): io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams.RequestedSchema = + io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams.RequestedSchema(properties, required) +} + +/** + * Sent from the server to create an elicitation from the client. + */ +@Deprecated( + message = "Use `ElicitRequest` instead", + replaceWith = ReplaceWith( + "ElicitRequest", + "io.modelcontextprotocol.kotlin.sdk.types.ElicitRequest", + ), + level = DeprecationLevel.ERROR, +) +public fun CreateElicitationRequest( + params: io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams, +): io.modelcontextprotocol.kotlin.sdk.types.ElicitRequest = + io.modelcontextprotocol.kotlin.sdk.types.ElicitRequest(params) + +@Deprecated( + message = "Use `ElicitResult` instead", + replaceWith = ReplaceWith( + "ElicitResult", + "io.modelcontextprotocol.kotlin.sdk.types.ElicitResult", + ), + level = DeprecationLevel.ERROR, +) +public object CreateElicitationResult { + @Deprecated( + message = "Use `ElicitResult.Action` instead", + replaceWith = ReplaceWith( + "ElicitResult.Action", + "io.modelcontextprotocol.kotlin.sdk.types.ElicitResult.Action", + ), + level = DeprecationLevel.ERROR, + ) + public object Action { + @Deprecated( + message = "Use `ElicitResult.Action.Accept` instead", + replaceWith = ReplaceWith( + "ElicitResult.Action.Accept", + "io.modelcontextprotocol.kotlin.sdk.types.ElicitResult.Action", + ), + level = DeprecationLevel.ERROR, + ) + public val accept: io.modelcontextprotocol.kotlin.sdk.types.ElicitResult.Action = + io.modelcontextprotocol.kotlin.sdk.types.ElicitResult.Action.Accept + + @Deprecated( + message = "Use `ElicitResult.Action.Decline` instead", + replaceWith = ReplaceWith( + "ElicitResult.Action.Decline", + "io.modelcontextprotocol.kotlin.sdk.types.ElicitResult.Action", + ), + level = DeprecationLevel.ERROR, + ) + public val decline: io.modelcontextprotocol.kotlin.sdk.types.ElicitResult.Action = + io.modelcontextprotocol.kotlin.sdk.types.ElicitResult.Action.Decline + + @Deprecated( + message = "Use `ElicitResult.Action.Cancel` instead", + replaceWith = ReplaceWith( + "ElicitResult.Action.Cancel", + "io.modelcontextprotocol.kotlin.sdk.types.ElicitResult.Action", + ), + level = DeprecationLevel.ERROR, + ) + public val cancel: io.modelcontextprotocol.kotlin.sdk.types.ElicitResult.Action = + io.modelcontextprotocol.kotlin.sdk.types.ElicitResult.Action.Cancel + } +} + +/** + * The client's response to an elicitation/create request from the server. + */ +@Deprecated( + message = "Use `ElicitResult` instead", + replaceWith = ReplaceWith( + "ElicitResult", + "io.modelcontextprotocol.kotlin.sdk.types.ElicitResult", + ), + level = DeprecationLevel.ERROR, +) +public fun CreateElicitationResult( + action: io.modelcontextprotocol.kotlin.sdk.types.ElicitResult.Action, + content: JsonObject? = null, + meta: JsonObject? = null, +): io.modelcontextprotocol.kotlin.sdk.types.ElicitResult = + io.modelcontextprotocol.kotlin.sdk.types.ElicitResult(action, content, meta) + +/** + * Represents an error specific to the MCP protocol. + */ +@Deprecated( + message = "Use `McpException` instead", + replaceWith = ReplaceWith("McpException"), + level = DeprecationLevel.ERROR, +) +public typealias McpError = io.modelcontextprotocol.kotlin.sdk.types.McpException 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 new file mode 100644 index 0000000..3e0abe1 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.util.kt @@ -0,0 +1,42 @@ +package io.modelcontextprotocol.kotlin.sdk + +import io.modelcontextprotocol.kotlin.sdk.types.error +import io.modelcontextprotocol.kotlin.sdk.types.success +import kotlinx.serialization.json.JsonObject + +@Deprecated( + message = "Use `io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject` instead", + replaceWith = ReplaceWith("EmptyJsonObject", "io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject"), + level = DeprecationLevel.ERROR, +) +public val EmptyJsonObject: JsonObject = io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject + +/** + * Creates a [CallToolResult] with single [TextContent] and [meta]. + */ +@Deprecated( + message = "Use `CallToolResult.success` instead", + replaceWith = ReplaceWith("CallToolResult.Companion.success"), + level = DeprecationLevel.ERROR, +) +@Suppress("DEPRECATION_ERROR") +public fun CallToolResult.ok( + content: String, + meta: JsonObject = EmptyJsonObject, +): io.modelcontextprotocol.kotlin.sdk.types.CallToolResult = + io.modelcontextprotocol.kotlin.sdk.types.CallToolResult.success(content, meta) + +/** + * Creates a [CallToolResult] with single [TextContent] and [meta], with `isError` being true. + */ +@Deprecated( + message = "Use `CallToolResult.error` instead", + replaceWith = ReplaceWith("CallToolResult.Companion.error"), + level = DeprecationLevel.ERROR, +) +@Suppress("DEPRECATION_ERROR") +public fun CallToolResult.error( + content: String, + meta: JsonObject = EmptyJsonObject, +): io.modelcontextprotocol.kotlin.sdk.types.CallToolResult = + io.modelcontextprotocol.kotlin.sdk.types.CallToolResult.error(content, meta) 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 index 6a9015b..f63f4a9 100644 --- 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 @@ -6,32 +6,14 @@ 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 + * @property code The MCP/JSON‑RPC error code. + * @property data Optional additional error payload as a JSON element; `null` when not provided. + * @param message The error message. + * @param cause The original cause. */ -public open class McpException @JvmOverloads public constructor( +public class McpException @JvmOverloads public constructor( public val code: Int, - message: String = "MCP error $code", + message: String, 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) - } - } -} +) : Exception("MCP error $code: $message", cause) 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 index 8280404..2f1e3ae 100644 --- 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 @@ -4,11 +4,6 @@ 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, @@ -17,7 +12,6 @@ public data class PingRequest(override val params: BaseRequestParams? = null) : @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 deleted file mode 100644 index f3da768..0000000 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredException.kt +++ /dev/null @@ -1,65 +0,0 @@ -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 index 0d8b2b7..b132a5e 100644 --- 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 @@ -21,7 +21,7 @@ import kotlinx.serialization.json.buildJsonObject * val request = buildInitializeRequest { * protocolVersion = "1.0" * capabilities { - * sampling(ClientCapabilities.Sampling()) + * sampling(ClientCapabilities.sampling) * roots(listChanged = true) * experimental { * put("customFeature", JsonPrimitive(true)) @@ -36,32 +36,45 @@ import kotlinx.serialization.json.buildJsonObject */ @McpDsl public class ClientCapabilitiesBuilder @PublishedApi internal constructor() { - private var sampling: ClientCapabilities.Sampling? = null + private var sampling: JsonObject? = null private var roots: ClientCapabilities.Roots? = null - private var elicitation: ClientCapabilities.Elicitation? = null - private var extensions: Map? = null + private var elicitation: JsonObject? = null private var experimental: JsonObject? = null /** - * Sampling capability configuration. See [ClientCapabilities.Sampling]. + * Indicates that the client supports sampling from an LLM. * - * 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. + * Use [ClientCapabilities.sampling] for default empty configuration. * * Example: * ```kotlin * capabilities { - * sampling(ClientCapabilities.Sampling(tools = EmptyJsonObject)) + * sampling(ClientCapabilities.sampling) * } * ``` * * @param value The sampling capability configuration */ - public fun sampling(value: ClientCapabilities.Sampling) { + public fun sampling(value: JsonObject) { this.sampling = value } + /** + * Indicates that the client supports sampling from an LLM with custom configuration. + * + * Example: + * ```kotlin + * capabilities { + * sampling { + * put("temperature", JsonPrimitive(0.7)) + * } + * } + * ``` + * + * @param block Lambda for building the sampling configuration + */ + public fun sampling(block: JsonObjectBuilder.() -> Unit): Unit = sampling(buildJsonObject(block)) + /** * Indicates that the client supports listing roots. * @@ -99,33 +112,25 @@ public class ClientCapabilitiesBuilder @PublishedApi internal constructor() { * * @param value The elicitation capability configuration */ - public fun elicitation(value: ClientCapabilities.Elicitation) { + public fun elicitation(value: JsonObject) { 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. + * Indicates that the client supports elicitation from the server with custom configuration. * * Example: * ```kotlin * capabilities { - * extensions(mapOf( - * "io.modelcontextprotocol/ui" to buildJsonObject { - * put("mimeTypes", JsonArray(listOf(JsonPrimitive("text/html")))) - * } - * )) + * elicitation { + * put("mode", JsonPrimitive("interactive")) + * } * } * ``` * - * @param value The map of extension identifiers to their settings + * @param block Lambda for building the elicitation configuration */ - public fun extensions(value: Map) { - this.extensions = value - } + public fun elicitation(block: JsonObjectBuilder.() -> Unit): Unit = elicitation(buildJsonObject(block)) /** * Defines experimental, non-standard capabilities that the client supports. @@ -166,13 +171,11 @@ public class ClientCapabilitiesBuilder @PublishedApi internal constructor() { */ 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 index ecb8c48..cff3b89 100644 --- 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 @@ -41,84 +41,22 @@ public data class Implementation( * 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 sampling: JsonObject? = null, public val roots: Roots? = null, - public val elicitation: Elicitation? = null, - public val tasks: Tasks? = null, + public val elicitation: JsonObject? = 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() + public val sampling: JsonObject = EmptyJsonObject + public val elicitation: JsonObject = EmptyJsonObject } /** @@ -133,71 +71,8 @@ public data class ClientCapabilities( */ @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. * @@ -213,11 +88,7 @@ public val ClientCapabilities.Elicitation?.supportsUrl: Boolean * @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( @@ -226,15 +97,9 @@ public data class ServerCapabilities( 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 @@ -277,35 +142,4 @@ public data class ServerCapabilities( */ @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 index f65b1ed..5e39d1f 100644 --- 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 @@ -8,16 +8,12 @@ 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" +public const val LATEST_PROTOCOL_VERSION: String = "2025-06-18" -/** 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", ) @@ -31,9 +27,17 @@ public val SUPPORTED_PROTOCOL_VERSIONS: List = listOf( */ @Serializable public sealed interface WithMeta { - /** Optional metadata attached to this entity. */ @SerialName("_meta") public val meta: JsonObject? + + @Deprecated( + message = "Use 'meta' instead.", + replaceWith = ReplaceWith("meta"), + level = DeprecationLevel.ERROR, + ) + @Suppress("PropertyName", "VariableNaming") + public val _meta: JsonObject + get() = meta ?: EmptyJsonObject } // ============================================================================ @@ -45,10 +49,8 @@ public sealed interface WithMeta { */ 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) // ============================================================================ @@ -90,6 +92,9 @@ public data class Icon( ) { /** * The theme context for which an icon is designed. + * + * @property Light Icon designed for use with a light background (typically darker icon). + * @property Dark Icon designed for use with a dark background (typically lighter icon). */ @Serializable public enum class Theme { @@ -127,15 +132,9 @@ public enum class Role { */ @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") 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 index 1eb5329..5ca41c6 100644 --- 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 @@ -37,9 +37,20 @@ public data class CompleteRequest(override val params: CompleteRequestParams) : public val context: Context? get() = params.context - /** Convenience accessor for [params]'s metadata. */ public val meta: RequestMeta? get() = params.meta + + @Deprecated( + message = "Will be removed in a future release.", + replaceWith = ReplaceWith(expression = "CompleteRequest(CompleteRequestParams(argument, ref, context, meta))"), + level = DeprecationLevel.ERROR, + ) + public constructor( + argument: Argument, + ref: Reference, + context: Context? = null, + meta: RequestMeta? = null, + ) : this(CompleteRequestParams(argument, ref, context, meta)) } /** @@ -48,8 +59,6 @@ public data class CompleteRequest(override val params: CompleteRequestParams) : * @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( 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 index 8718dec..e538af5 100644 --- 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 @@ -88,7 +88,6 @@ public abstract class MediaContentBuilder { meta = buildJsonObject(block) } - /** Constructs the [MediaContent] instance from the current builder state. */ @PublishedApi internal abstract fun build(): MediaContent } 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 index 815e5f6..0e6377f 100644 --- 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 @@ -8,11 +8,6 @@ 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") @@ -29,45 +24,15 @@ public enum class ContentTypes(public val value: String) { @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 -} +public sealed interface MediaContent : ContentBlock /** * Text provided to or from an LLM. @@ -196,50 +161,3 @@ public data class EmbeddedResource( @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 deleted file mode 100644 index d486b1f..0000000 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/discovery.kt +++ /dev/null @@ -1,107 +0,0 @@ -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 index 12309fa..e398d98 100644 --- 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 @@ -4,7 +4,6 @@ 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 @@ -224,9 +223,6 @@ public class ElicitRequestedSchemaBuilder @PublishedApi internal constructor() { 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) + return ElicitRequestParams.RequestedSchema(properties, 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 index 1bb73e6..dfda9bf 100644 --- 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 @@ -1,20 +1,23 @@ 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 an `elicitation/create` request from the server to the client. + * A request from the server to elicit additional information from the user via 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. + * This request type allows servers to prompt users for structured input through forms + * or dialogs presented by the client. The server defines a schema for the requested data, + * and the client presents an appropriate UI to collect this information. * - * @property params The elicitation parameters — either form or URL mode. + * @property params The elicitation parameters including the message and requested schema. */ @Serializable public data class ElicitRequest(override val params: ElicitRequestParams) : ServerRequest { + @OptIn(ExperimentalSerializationApi::class) @EncodeDefault public override val method: Method = Method.Defined.ElicitationCreate @@ -27,13 +30,8 @@ public data class ElicitRequest(override val params: ElicitRequestParams) : Serv /** * 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 + public val requestedSchema: ElicitRequestParams.RequestedSchema + get() = params.requestedSchema /** * Metadata for this request. May include a progressToken for out-of-band progress notifications. @@ -43,14 +41,23 @@ public data class ElicitRequest(override val params: ElicitRequestParams) : Serv } /** - * Represents the parameters for an `elicitation/create` request. + * Parameters for an elicitation/create request. * - * Implementations: [ElicitRequestFormParams], [ElicitRequestURLParams]. + * @property message The message to present to the user. This should clearly explain + * what information is being requested and why. + * @property requestedSchema A restricted subset of JSON Schema defining the structure + * of the requested data. Only top-level properties are allowed, + * without nesting. + * @property meta Optional metadata for this request. May include a progressToken for + * out-of-band progress notifications. */ -@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 +@Serializable +public data class ElicitRequestParams( + val message: String, + val requestedSchema: RequestedSchema, + @SerialName("_meta") + override val meta: RequestMeta? = null, +) : RequestParams { /** * A restricted JSON Schema for elicitation requests. @@ -58,117 +65,31 @@ public sealed interface ElicitRequestParams : RequestParams { * 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. + * @property type Always "object" for elicitation schemas. */ @Serializable - public data class RequestedSchema( - @SerialName($$"$schema") - val schema: String? = null, - val properties: Map, - val required: List? = null, - ) { - /** Always "object" for elicitation schemas. */ + public data class RequestedSchema(val properties: JsonObject, val required: List? = null) { + @OptIn(ExperimentalSerializationApi::class) @EncodeDefault val type: String = "object" } } /** - * Creates an [ElicitRequestFormParams] for backwards compatibility. + * The client's response to an [ElicitRequest]. * - * @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. + * Represents the user's action and, if accepted, the submitted form data. * - * 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 action The user action in response to the elicitation prompt. + * @property content The submitted form data, only present when [action] is [Action.Accept]. + * Contains values matching the requested schema, where keys correspond + * to property names and values are primitives (string, number, or boolean). * @property meta Optional metadata for this response. + * @throws IllegalArgumentException if content is provided with a non-accept action. */ @Serializable public data class ElicitResult( @@ -186,6 +107,10 @@ public data class ElicitResult( /** * The user's response action to an elicitation request. + * + * @property Accept User submitted the form/confirmed the action. Content will be provided. + * @property Decline User explicitly declined the action. No content provided. + * @property Cancel User dismissed the dialog without making an explicit choice. No content provided. */ @Serializable public enum class Action { 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 index f3e1575..2cb58b7 100644 --- 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 @@ -21,7 +21,7 @@ import kotlin.contracts.contract * val request = buildInitializeRequest { * protocolVersion = "2024-11-05" * capabilities { - * sampling(ClientCapabilities.Sampling()) + * sampling(ClientCapabilities.sampling) * roots(listChanged = true) * } * info("MyClient", "1.0.0") @@ -33,7 +33,7 @@ import kotlin.contracts.contract * val request = buildInitializeRequest { * protocolVersion = "2024-11-05" * capabilities { - * sampling(ClientCapabilities.Sampling()) + * sampling(ClientCapabilities.sampling) * experimental { * put("feature", JsonPrimitive(true)) * } @@ -94,7 +94,7 @@ public class InitializeRequestBuilder @PublishedApi internal constructor() : Req * Example: * ```kotlin * capabilities(ClientCapabilities( - * sampling = ClientCapabilities.Sampling(), + * sampling = ClientCapabilities.sampling, * roots = ClientCapabilities.Roots(listChanged = true) * )) * ``` @@ -113,7 +113,7 @@ public class InitializeRequestBuilder @PublishedApi internal constructor() : Req * Example: * ```kotlin * capabilities { - * sampling(ClientCapabilities.Sampling()) + * sampling(ClientCapabilities.sampling) * roots(listChanged = true) * elicitation(ClientCapabilities.elicitation) * } 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 index 7e10904..68b2f02 100644 --- 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 @@ -14,7 +14,6 @@ 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" /** @@ -41,20 +40,10 @@ public fun RequestId(value: Long): RequestId = RequestId.NumberId(value) @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 @@ -107,13 +96,9 @@ internal fun JSONRPCNotification.fromJSON(): Notification = */ @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 @@ -130,6 +115,7 @@ public data object JSONRPCEmptyMessage : JSONRPCMessage { * The server or client (depending on a direction) must respond with either a * [JSONRPCResponse] or [JSONRPCError] that has the same [id]. * + * @property jsonrpc Always "2.0" to indicate JSON-RPC 2.0 protocol. * @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"). @@ -142,7 +128,6 @@ public data class JSONRPCRequest @JvmOverloads public constructor( 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 @@ -185,13 +170,13 @@ public data class JSONRPCRequest @JvmOverloads public constructor( * * Examples: progress updates, resource change notifications, log messages. * + * @property jsonrpc Always "2.0" to indicate JSON-RPC 2.0 protocol. * @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 } @@ -206,12 +191,12 @@ public data class JSONRPCNotification(val method: String, val params: JsonElemen * Sent in response to a [JSONRPCRequest] when the method execution succeeds. * The [id] must match the [id] of the original request. * + * @property jsonrpc Always "2.0" to indicate JSON-RPC 2.0 protocol. * @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 } @@ -226,12 +211,12 @@ public data class JSONRPCResponse(val id: RequestId, val result: RequestResult = * Sent in response to a [JSONRPCRequest] when the method execution fails. * The [id] must match the [id] of the original request. * + * @property jsonrpc Always "2.0" to indicate JSON-RPC 2.0 protocol. * @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 } @@ -266,19 +251,6 @@ public data class RPCError(val code: Int, val message: String, val data: JsonEle /** 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 */ 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 index d83e012..566905f 100644 --- 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 @@ -11,10 +11,8 @@ 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 { 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 index 1824ad0..9970015 100644 --- 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 @@ -7,7 +7,6 @@ import kotlinx.serialization.Serializable */ @Serializable(with = MethodSerializer::class) public sealed interface Method { - /** The string representation of this method name. */ public val value: String /** @@ -16,7 +15,6 @@ public sealed interface Method { @Serializable public enum class Defined(override val value: String) : Method { Initialize("initialize"), - ServerDiscover("server/discover"), Ping("ping"), ResourcesList("resources/list"), ResourcesTemplatesList("resources/templates/list"), @@ -34,8 +32,6 @@ public sealed interface Method { 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"), @@ -43,10 +39,6 @@ public sealed interface Method { CompletionComplete("completion/complete"), RootsList("roots/list"), ElicitationCreate("elicitation/create"), - TasksGet("tasks/get"), - TasksResult("tasks/result"), - TasksList("tasks/list"), - TasksCancel("tasks/cancel"), } /** @@ -54,13 +46,4 @@ public sealed interface Method { */ @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 index 3d33c21..8eb4e4b 100644 --- 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 @@ -14,10 +14,7 @@ import kotlinx.serialization.json.JsonObject */ @Serializable(with = NotificationPolymorphicSerializer::class) public sealed interface Notification { - /** The notification method identifier. */ public val method: Method - - /** Optional notification parameters. */ public val params: NotificationParams? } @@ -34,7 +31,9 @@ public sealed interface ClientNotification : Notification public sealed interface ServerNotification : Notification /** - * Interface for notification parameter types. Inherits the optional `_meta` field from [WithMeta]. + * Interface for notification parameter types. + * + * @property meta Optional metadata for the notification. */ @Serializable public sealed interface NotificationParams : WithMeta @@ -80,7 +79,6 @@ public data class CustomNotification(override val method: Method, override val p ClientNotification, ServerNotification { - /** Convenience accessor for [params]'s metadata. */ public val meta: JsonObject? get() = params?.meta } @@ -332,36 +330,6 @@ public data class RootsListChangedNotification(override val params: BaseNotifica 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 // ============================================================================ @@ -380,48 +348,3 @@ public data class ToolListChangedNotification(override val params: BaseNotificat @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/prompts.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/prompts.kt index 5d7054b..08a7a9f 100644 --- 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 @@ -113,6 +113,23 @@ public data class GetPromptRequest(override val params: GetPromptRequestParams) @EncodeDefault override val method: Method = Method.Defined.PromptsGet + @Deprecated( + message = "Use constructor with GetPromptRequestParams instead", + replaceWith = ReplaceWith("GetPromptRequest(GetPromptRequestParams(name, arguments, meta))"), + level = DeprecationLevel.ERROR, + ) + public constructor( + name: String, + arguments: Map? = null, + meta: RequestMeta? = null, + ) : this( + GetPromptRequestParams( + name = name, + arguments = arguments, + meta = meta, + ), + ) + /** * The name of the prompt or prompt template to retrieve. */ @@ -187,6 +204,26 @@ public data class ListPromptsRequest(override val params: PaginatedRequestParams PaginatedRequest { @EncodeDefault override val method: Method = Method.Defined.PromptsList + + /** + * Secondary constructor for creating a [ListPromptsRequest] instance + * using optional cursor and metadata parameters. + * + * This constructor simplifies the creation of the [ListPromptsRequest] by allowing a cursor + * and metadata to be provided. + * + * @param cursor Optional cursor string to specify the starting point of the paginated request. + * @param meta Optional metadata associated with the request. + */ + @Deprecated( + message = "Use constructor with ListPromptsParams instead", + replaceWith = ReplaceWith("ListPromptsRequest(ListPromptsParams(name, arguments, meta))"), + level = DeprecationLevel.ERROR, + ) + public constructor( + cursor: String?, + meta: RequestMeta? = null, + ) : this(paginatedRequestParams(cursor, meta)) } /** 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 index 7d885fb..699dfce 100644 --- 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 @@ -1,136 +1,27 @@ 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) + when (element) { + is JsonPrimitive if (element.isString) -> ProgressToken(element.content) + is JsonPrimitive if (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. * @@ -140,9 +31,6 @@ public value class RequestMeta(public val json: JsonObject) { public operator fun get(key: String): JsonElement? = json[key] } -/** - * Base interface for parameters attached to a [Request]. - */ @Serializable public sealed interface RequestParams { /** @@ -155,9 +43,6 @@ public sealed interface RequestParams { public val meta: RequestMeta? } -/** - * Default [RequestParams] implementation carrying only optional metadata. - */ @Serializable public data class BaseRequestParams(@SerialName("_meta") override val meta: RequestMeta? = null) : RequestParams @@ -167,10 +52,7 @@ public data class BaseRequestParams(@SerialName("_meta") override val meta: Requ @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? } @@ -218,22 +100,16 @@ public sealed interface PaginatedRequest : Request { @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. + * @param meta Additional metadata for the response. Defaults to an empty JSON object. */ @Serializable public data class EmptyResult(@SerialName("_meta") override val meta: JsonObject? = null) : @@ -241,11 +117,10 @@ public data class EmptyResult(@SerialName("_meta") override val meta: JsonObject ServerResult /** - * Represents a paginated result. + * Represents a request supporting pagination. */ @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? } 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 index 1afa0e6..8782db1 100644 --- 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 @@ -8,9 +8,6 @@ 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 @@ -117,14 +114,15 @@ public data class ResourceTemplateReference(val uri: String) : Reference { } /** - * The contents of a specific resource or sub-resource. Carries the optional `_meta` field via [WithMeta]. + * The contents of a specific resource or sub-resource. + * + * @property uri The URI of this resource. + * @property mimeType The MIME type of this resource, if known. + * @property meta Optional metadata for this response. */ @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? } @@ -135,7 +133,6 @@ public sealed interface ResourceContents : WithMeta { * 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( @@ -152,7 +149,6 @@ public data class TextResourceContents( * @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( @@ -168,7 +164,6 @@ public data class BlobResourceContents( * * @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( @@ -196,6 +191,26 @@ public data class ListResourcesRequest(override val params: PaginatedRequestPara PaginatedRequest { @EncodeDefault override val method: Method = Method.Defined.ResourcesList + + /** + * Secondary constructor for creating a [ListResourcesRequest] instance + * using optional cursor and metadata parameters. + * + * This constructor simplifies the creation of the [ListResourcesRequest] by allowing a cursor + * and metadata to be provided. + * + * @param cursor Optional cursor string to specify the starting point of the paginated request. + * @param meta Optional metadata associated with the request. + */ + @Deprecated( + message = "Use the constructor with PaginatedRequestParams property instead", + replaceWith = ReplaceWith("ReadResourceRequest(PaginatedRequestParams(cursor, meta))"), + level = DeprecationLevel.ERROR, + ) + public constructor( + cursor: String?, + meta: RequestMeta? = null, + ) : this(paginatedRequestParams(cursor, meta)) } /** @@ -246,6 +261,16 @@ public data class ReadResourceRequest(override val params: ReadResourceRequestPa */ public val meta: RequestMeta? get() = params.meta + + @Deprecated( + message = "Use the constructor with ReadResourceRequestParams property instead", + replaceWith = ReplaceWith("ReadResourceRequest(ReadResourceRequestParams(uri, meta))"), + level = DeprecationLevel.ERROR, + ) + public constructor( + uri: String, + meta: RequestMeta? = null, + ) : this(ReadResourceRequestParams(uri, meta)) } /** @@ -312,6 +337,16 @@ public data class SubscribeRequest(override val params: SubscribeRequestParams) */ public val meta: RequestMeta? get() = params.meta + + @Deprecated( + message = "Use the constructor with SubscribeRequestParams property instead", + replaceWith = ReplaceWith("ReadResourceRequest(SubscribeRequestParams(uri, meta))"), + level = DeprecationLevel.ERROR, + ) + public constructor( + uri: String, + meta: RequestMeta? = null, + ) : this(SubscribeRequestParams(uri, meta)) } /** @@ -398,6 +433,26 @@ public data class ListResourceTemplatesRequest(override val params: PaginatedReq PaginatedRequest { @EncodeDefault override val method: Method = Method.Defined.ResourcesTemplatesList + + /** + * Secondary constructor for creating a [ListResourceTemplatesRequest] instance + * using optional cursor and metadata parameters. + * + * This constructor simplifies the creation of the [ListResourceTemplatesRequest] by allowing a cursor + * and metadata to be provided. + * + * @param cursor Optional cursor string to specify the starting point of the paginated request. + * @param meta Optional metadata associated with the request. + */ + @Deprecated( + message = "Use the constructor with PaginatedRequestParams property instead", + replaceWith = ReplaceWith("ListResourceTemplatesRequest(PaginatedRequestParams(cursor, meta))"), + level = DeprecationLevel.ERROR, + ) + public constructor( + cursor: String?, + meta: RequestMeta? = null, + ) : this(paginatedRequestParams(cursor, meta)) } /** 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 index 7ee4f9e..9e7dbae 100644 --- 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 @@ -60,9 +60,17 @@ public data class ListRootsRequest(override val params: BaseRequestParams? = nul @EncodeDefault override val method: Method = Method.Defined.RootsList - /** Convenience accessor for [params]'s metadata. */ public val meta: RequestMeta? get() = params?.meta + + @Deprecated( + message = "Use the constructor with BaseRequestParams property instead", + replaceWith = ReplaceWith("ListRootsRequest(BaseRequestParams(meta))"), + level = DeprecationLevel.ERROR, + ) + public constructor( + meta: RequestMeta?, + ) : this(BaseRequestParams(meta)) } /** 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 index 4752ff5..1bc00e2 100644 --- 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 @@ -112,8 +112,8 @@ public class CreateMessageRequestBuilder @PublishedApi internal constructor() : * Example: * ```kotlin * messages(listOf( - * SamplingMessage(Role.User, listOf(TextContent("Hello"))), - * SamplingMessage(Role.Assistant, listOf(TextContent("Hi!"))) + * SamplingMessage(Role.User, TextContent("Hello")), + * SamplingMessage(Role.Assistant, TextContent("Hi!")) * )) * ``` */ @@ -246,7 +246,7 @@ public class SamplingMessageBuilder @PublishedApi internal constructor() { * ``` */ public fun user(content: MediaContent) { - messages.add(SamplingMessage(Role.User, listOf(content))) + messages.add(SamplingMessage(Role.User, content)) } /** @@ -258,7 +258,7 @@ public class SamplingMessageBuilder @PublishedApi internal constructor() { * ``` */ public fun assistant(content: MediaContent) { - messages.add(SamplingMessage(Role.Assistant, listOf(content))) + messages.add(SamplingMessage(Role.Assistant, content)) } @PublishedApi 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 index 7cc2685..41dc864 100644 --- 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 @@ -75,37 +75,11 @@ public data class ModelPreferences( * * 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. + * @property content The content of the message. Can be text, image, or audio content. */ @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) -} +public data class SamplingMessage(val role: Role, val content: MediaContent) // ============================================================================ // sampling/createMessage @@ -182,12 +156,6 @@ public data class CreateMessageRequest(override val params: CreateMessageRequest 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. */ @@ -216,14 +184,6 @@ public data class CreateMessageRequest(override val params: CreateMessageRequest * @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. */ @@ -237,9 +197,6 @@ public data class CreateMessageRequestParams( 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 @@ -269,86 +226,29 @@ public enum class IncludeContext { * 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 content The generated content. Can be text, image, or audio content. + * @property model The name of the model that generated the message (e.g., "claude-3-opus-20240229", + * "gpt-4-turbo-preview"). This helps the server understand which model was used. * @property stopReason The reason why sampling stopped, if known. - * Common values: [StopReason.EndTurn], [StopReason.StopSequence], [StopReason.MaxTokens], - * [StopReason.ToolUse]. + * Common values: "end_turn", "max_tokens", "stop_sequence", "content_filter" * @property meta Optional metadata for this response. */ @Serializable public data class CreateMessageResult( val role: Role, - @Serializable(with = SamplingContentSerializer::class) - val content: List, + val content: MediaContent, 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" } - } +) : ClientResult - /** - * 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 deleted file mode 100644 index 0982500..0000000 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/schema.kt +++ /dev/null @@ -1,302 +0,0 @@ -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 index 36b7e37..a00b26b 100644 --- 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 @@ -1,24 +1,17 @@ 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 @@ -60,11 +53,18 @@ internal object MethodSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("io.modelcontextprotocol.kotlin.sdk.types.Method", PrimitiveKind.STRING) + private val definedMethods: Map by lazy { + Method.Defined.entries.associateBy { it.value } + } + override fun serialize(encoder: Encoder, value: Method) { encoder.encodeString(value.value) } - override fun deserialize(decoder: Decoder): Method = Method.from(decoder.decodeString()) + override fun deserialize(decoder: Decoder): Method { + val decodedString = decoder.decodeString() + return definedMethods[decodedString] ?: Method.Custom(decodedString) + } } // ============================================================================ @@ -120,101 +120,6 @@ internal object MediaContentPolymorphicSerializer : } } -/** - * 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 // ============================================================================ @@ -239,11 +144,9 @@ internal object ResourceContentsPolymorphicSerializer : // 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(), @@ -256,10 +159,6 @@ private val clientRequestDeserializers: Map 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 } } @@ -420,16 +309,9 @@ private fun selectClientResultDeserializer(element: JsonElement): Deserializatio * 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() @@ -439,9 +321,6 @@ private fun selectServerResultDeserializer(element: JsonElement): Deserializatio "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 } } @@ -492,23 +371,21 @@ internal object ServerResultPolymorphicSerializer : * Polymorphic serializer for [JSONRPCMessage] types. * Determines the message type based on the presence of specific fields: * - "error" -> JSONRPCError - * - "result" + "id" -> JSONRPCResponse - * - "result" -> JSONRPCEmptyMessage + * - "result" -> JSONRPCResponse * - "method" + "id" -> JSONRPCRequest * - "method" -> JSONRPCNotification */ internal object JSONRPCMessagePolymorphicSerializer : JsonContentPolymorphicSerializer(JSONRPCMessage::class) { override fun selectDeserializer(element: JsonElement): DeserializationStrategy { - val jsonObj = element.jsonObject + val jsonObject = 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}") + "error" in jsonObject -> JSONRPCError.serializer() + "result" in jsonObject -> JSONRPCResponse.serializer() + "method" in jsonObject && "id" in jsonObject -> JSONRPCRequest.serializer() + "method" in jsonObject -> JSONRPCNotification.serializer() + jsonObject.isEmpty() || jsonObject.keys == setOf("jsonrpc") -> JSONRPCEmptyMessage.serializer() + else -> throw SerializationException("Invalid JSONRPCMessage type: ${jsonObject.keys}") } } } @@ -518,83 +395,9 @@ internal object JSONRPCMessagePolymorphicSerializer : * 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() + override fun selectDeserializer(element: JsonElement): DeserializationStrategy = when (element) { + is JsonPrimitive if (element.isString) -> RequestId.StringId.serializer() + is JsonPrimitive if (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 deleted file mode 100644 index 7363561..0000000 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tasks.kt +++ /dev/null @@ -1,361 +0,0 @@ -@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.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.kt index 1f0f54e..b7aade5 100644 --- 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 @@ -39,14 +39,12 @@ public fun CallToolResult.Companion.error(content: String, meta: JsonObject? = n * 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. @@ -57,8 +55,6 @@ public fun CallToolResult.Companion.error(content: String, meta: JsonObject? = n * @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 @@ -70,7 +66,6 @@ public data class Tool( val title: String? = null, val annotations: ToolAnnotations? = null, val icons: List? = null, - val execution: ToolExecution? = null, @SerialName("_meta") override val meta: JsonObject? = null, ) : WithMeta @@ -79,62 +74,24 @@ public data class Tool( * 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 type Always "object" for tool schemas. * @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. * @@ -203,12 +160,6 @@ public data class CallToolRequest(override val params: CallToolRequestParams) : 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. */ @@ -222,8 +173,6 @@ public data class CallToolRequest(override val params: CallToolRequestParams) : * @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. */ @@ -231,7 +180,6 @@ public data class CallToolRequest(override val params: CallToolRequestParams) : public data class CallToolRequestParams( val name: String, val arguments: JsonObject? = null, - val task: TaskMetadata? = null, @SerialName("_meta") override val meta: RequestMeta? = null, ) : RequestParams @@ -284,6 +232,26 @@ public data class ListToolsRequest(override val params: PaginatedRequestParams? PaginatedRequest { @EncodeDefault override val method: Method = Method.Defined.ToolsList + + /** + * Secondary constructor for creating a [ListToolsRequest] instance + * using optional cursor and metadata parameters. + * + * This constructor simplifies the creation of the [ListToolsRequest] by allowing a cursor + * and metadata to be provided. + * + * @param cursor Optional cursor string to specify the starting point of the paginated request. + * @param meta Optional metadata associated with the request. + */ + @Deprecated( + message = "Use the constructor with BaseRequestParams property instead", + replaceWith = ReplaceWith("ListToolsRequest(BaseRequestParams(meta))"), + level = DeprecationLevel.ERROR, + ) + public constructor( + cursor: String?, + meta: RequestMeta? = null, + ) : this(paginatedRequestParams(cursor, meta)) } /** 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 deleted file mode 100644 index aa2abb9..0000000 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcher.kt +++ /dev/null @@ -1,154 +0,0 @@ -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 deleted file mode 100644 index d99bfda..0000000 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcher.kt +++ /dev/null @@ -1,68 +0,0 @@ -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 deleted file mode 100644 index 452333b..0000000 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.kt +++ /dev/null @@ -1,16 +0,0 @@ -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/shared/ProtocolConcurrencyTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolConcurrencyTest.kt deleted file mode 100644 index b3c95ac..0000000 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolConcurrencyTest.kt +++ /dev/null @@ -1,524 +0,0 @@ -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 deleted file mode 100644 index 81bf24a..0000000 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptionsTest.kt +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index 38ad556..0000000 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOutboundCancellationTest.kt +++ /dev/null @@ -1,167 +0,0 @@ -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 index 2846ccc..b228071 100644 --- 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 @@ -1,31 +1,20 @@ 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.channels.Channel import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObjectBuilder @@ -35,10 +24,8 @@ 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 @@ -139,36 +126,6 @@ class ProtocolTest { 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) @@ -194,174 +151,58 @@ class ProtocolTest { transport.deliver(JSONRPCResponse(sent.id, EmptyResult())) inFlight.await() } +} - @Test - fun `request handler receives enriched extra and ambient context element`() = runTest { - protocol.connect(transport) +private class TestProtocol : Protocol(null) { + override fun assertCapabilityForMethod(method: Method) { + // noop + } + override fun assertNotificationCapability(method: Method) { + // noop + } + override fun assertRequestHandlerCapability(method: Method) { + // noop + } +} - var seenByParameter: RequestHandlerExtra? = null - var seenAmbient: RequestHandlerExtra? = null - protocol.fallbackRequestHandler = { _, extra -> - seenByParameter = extra - seenAmbient = currentRequestHandlerExtra() - EmptyResult() - } +private class RecordingTransport : Transport { + private val sentMessages = Channel(Channel.UNLIMITED) + private var onMessageCallback: (suspend (JSONRPCMessage) -> Unit)? = null + private var onCloseCallback: (() -> Unit)? = null - 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 + override suspend fun start() { + // noop } - @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 + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { + sentMessages.send(message) } - @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) + override suspend fun close() { + onCloseCallback?.invoke() } - @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() + override fun onClose(block: () -> Unit) { + onCloseCallback = block } - @Test - fun `currentRequestHandlerExtra returns null outside handlers`() = runTest { - currentRequestHandlerExtra() shouldBe null + override fun onError(block: (Throwable) -> Unit) { + // noop } - @Test - fun `connect while already connected throws IllegalStateException`() = runTest { - protocol.connect(transport) - shouldThrow { - protocol.connect(RecordingTransport()) - } + override fun onMessage(block: suspend (JSONRPCMessage) -> Unit) { + onMessageCallback = block } - @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 + suspend fun awaitRequest(): JSONRPCRequest { + val message = sentMessages.receive() + return message as? JSONRPCRequest + ?: error("Expected JSONRPCRequest but received ${message::class.simpleName}") } - @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" + suspend fun deliver(message: JSONRPCMessage) { + val callback = onMessageCallback ?: error("onMessage callback not registered") + callback(message) } } 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 deleted file mode 100644 index 9e8dd60..0000000 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolTestFakes.kt +++ /dev/null @@ -1,112 +0,0 @@ -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 e5e50f8..7b4f053 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 @@ -6,9 +6,7 @@ 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 { @@ -77,35 +75,4 @@ 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 index 3468aae..33d5653 100644 --- 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 @@ -1,7 +1,6 @@ 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 @@ -9,7 +8,6 @@ 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 { @@ -121,7 +119,7 @@ class CapabilitiesTest { @Test fun `should serialize ClientCapabilities with sampling`() { val capabilities = ClientCapabilities( - sampling = ClientCapabilities.Sampling(), + sampling = ClientCapabilities.sampling, ) verifySerialization( capabilities, @@ -184,53 +182,6 @@ class CapabilitiesTest { ) } - @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 { @@ -264,15 +215,11 @@ class CapabilitiesTest { val experimental = buildJsonObject { put("feature1", buildJsonObject { put("enabled", true) }) } - val extensions = mapOf( - "io.modelcontextprotocol/ui" to EmptyJsonObject, - ) val capabilities = ClientCapabilities( - sampling = ClientCapabilities.Sampling(), + sampling = ClientCapabilities.sampling, roots = ClientCapabilities.Roots(listChanged = true), elicitation = ClientCapabilities.elicitation, experimental = experimental, - extensions = extensions, ) verifySerialization( capabilities, @@ -288,9 +235,6 @@ class CapabilitiesTest { "feature1": { "enabled": true } - }, - "extensions": { - "io.modelcontextprotocol/ui": {} } } """.trimIndent(), @@ -311,9 +255,9 @@ class CapabilitiesTest { val capabilities = verifyDeserialization(McpJson, json) - assertEquals(ClientCapabilities.Sampling(), capabilities.sampling) + assertEquals(EmptyJsonObject, capabilities.sampling) assertEquals(true, capabilities.roots?.listChanged) - assertEquals(ClientCapabilities.Elicitation(), capabilities.elicitation) + assertEquals(EmptyJsonObject, capabilities.elicitation) } @Test @@ -326,20 +270,14 @@ class CapabilitiesTest { 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(), + 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) @@ -528,32 +466,6 @@ class CapabilitiesTest { ) } - @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 { @@ -587,9 +499,6 @@ class CapabilitiesTest { 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( @@ -600,7 +509,6 @@ class CapabilitiesTest { logging = ServerCapabilities.Logging, completions = ServerCapabilities.Completions, experimental = experimental, - extensions = extensions, ) verifySerialization( capabilities, @@ -623,9 +531,6 @@ class CapabilitiesTest { "feature1": { "enabled": true } - }, - "extensions": { - "io.modelcontextprotocol/ui": {} } } """.trimIndent(), @@ -673,7 +578,6 @@ class CapabilitiesTest { assertNull(capabilities.logging) assertNull(capabilities.completions) assertNull(capabilities.experimental) - assertNull(capabilities.extensions) } @Test @@ -686,9 +590,6 @@ class CapabilitiesTest { ), prompts = ServerCapabilities.Prompts(listChanged = true), logging = ServerCapabilities.Logging, - extensions = mapOf( - "io.modelcontextprotocol/ui" to EmptyJsonObject, - ), ) verifySerializationRoundTrip(original, McpJson) @@ -736,11 +637,10 @@ class CapabilitiesTest { } """.trimIndent() - // Sampling is now a typed struct; unknown fields are ignored on deserialization - val capabilities = McpJson.decodeFromString(json) + val capabilities = verifyDeserialization(McpJson, json) - // Should not fail - additionalProperties are allowed (unknown fields are ignored by Sampling) - assertNotNull(capabilities.sampling) + // Should not fail - additionalProperties are allowed + assertEquals("customValue", capabilities.sampling?.get("customProperty")?.toString()?.trim('"')) } @Test @@ -758,189 +658,4 @@ class CapabilitiesTest { // 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 index a29a19c..44e88ac 100644 --- 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 @@ -14,14 +14,13 @@ class CommonTypeTest { @Test fun `should have correct latest protocol version`() { - LATEST_PROTOCOL_VERSION shouldBe "2025-11-25" + LATEST_PROTOCOL_VERSION shouldBe "2025-06-18" } @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", ) 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 index c6dee5d..f110326 100644 --- 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 @@ -1,26 +1,17 @@ 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!") @@ -317,133 +308,4 @@ class ContentTest { 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 deleted file mode 100644 index 691af45..0000000 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/DiscoveryTest.kt +++ /dev/null @@ -1,324 +0,0 @@ -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 index bd5ef06..0026ea0 100644 --- 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 @@ -1,40 +1,58 @@ 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.jsonObject 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.assertNotNull +import kotlin.test.assertNull class ElicitationTest { - // ── Form mode ─────────────────────────────────────────────────────── - @Test - fun `should serialize form mode ElicitRequest`() { + @Suppress("LongMethod") + fun `should serialize ElicitRequest with requested schema`() { val request = ElicitRequest( - ElicitRequestFormParams( + ElicitRequestParams( 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?"), - ), + properties = buildJsonObject { + put( + "owner", + buildJsonObject { + put("type", "string") + put("description", "GitHub organization") + }, + ) + put( + "repository", + buildJsonObject { + put("type", "string") + put("title", "Repository name") + }, + ) + put( + "private", + buildJsonObject { + put("type", "boolean") + put("description", "Is the repository private?") + }, + ) + }, required = listOf("owner", "repository"), ), meta = RequestMeta( - buildJsonObject { put("progressToken", "token-42") }, + buildJsonObject { + put("progressToken", "token-42") + }, ), ), ) @@ -50,16 +68,16 @@ class ElicitationTest { "requestedSchema": { "properties": { "owner": { - "description": "GitHub organization", - "type": "string" + "type": "string", + "description": "GitHub organization" }, "repository": { - "title": "Repository name", - "type": "string" + "type": "string", + "title": "Repository name" }, "private": { - "description": "Is the repository private?", - "type": "boolean" + "type": "boolean", + "description": "Is the repository private?" } }, "required": ["owner", "repository"], @@ -67,8 +85,7 @@ class ElicitationTest { }, "_meta": { "progressToken": "token-42" - }, - "mode": "form" + } } } """.trimIndent(), @@ -76,16 +93,22 @@ class ElicitationTest { } @Test - fun `should deserialize form mode ElicitRequest with explicit mode`() { + fun `should deserialize ElicitRequest from JSON`() { val json = """ { "method": "elicitation/create", "params": { - "mode": "form", - "message": "Enter your name", + "message": "Tell us about the repository", "requestedSchema": { "properties": { - "name": {"type": "string", "title": "Full name"} + "name": { + "type": "string", + "title": "Repository name" + }, + "stars": { + "type": "number", + "description": "GitHub stars" + } }, "type": "object" } @@ -94,118 +117,27 @@ class ElicitationTest { """.trimIndent() val request = verifyDeserialization(McpJson, json) - request.method shouldBe Method.Defined.ElicitationCreate - request.message shouldBe "Enter your name" + assertEquals(Method.Defined.ElicitationCreate, request.method) - val params = request.params.shouldBeInstanceOf() - val nameSchema = params.requestedSchema.properties["name"].shouldBeInstanceOf() - nameSchema.title shouldBe "Full name" + val params = request.params + assertEquals("Tell us about the repository", params.message) + assertNull(params.meta) + + val schema = params.requestedSchema + assertEquals("object", schema.type) + assertNull(schema.required) + + val nameDefinition = schema.properties["name"]?.jsonObject + assertNotNull(nameDefinition) + assertEquals("string", nameDefinition["type"]?.jsonPrimitive?.content) + assertEquals("Repository name", nameDefinition["title"]?.jsonPrimitive?.content) + + val starsDefinition = schema.properties["stars"]?.jsonObject + assertNotNull(starsDefinition) + assertEquals("number", starsDefinition["type"]?.jsonPrimitive?.content) + assertEquals("GitHub stars", starsDefinition["description"]?.jsonPrimitive?.content) } - @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( @@ -237,15 +169,17 @@ class ElicitationTest { """.trimIndent() val decoded = verifyDeserialization(McpJson, json) - decoded.action shouldBe ElicitResult.Action.Accept + assertEquals(ElicitResult.Action.Accept, decoded.action) - 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 content = decoded.content + assertNotNull(content) + assertEquals("kotlin-sdk", content["repository"]?.jsonPrimitive?.content) + assertEquals(128, content["stars"]?.jsonPrimitive?.int) + assertEquals(false, content["private"]?.jsonPrimitive?.boolean) - val meta = decoded.meta.shouldNotBeNull() - meta["submittedAt"]?.jsonPrimitive?.content shouldBe "2025-01-12T15:00:58Z" + val meta = decoded.meta + assertNotNull(meta) + assertEquals("2025-01-12T15:00:58Z", meta["submittedAt"]?.jsonPrimitive?.content) } @Test @@ -260,100 +194,18 @@ class ElicitationTest { """.trimIndent() val result = verifyDeserialization(McpJson, json) - result.action shouldBe ElicitResult.Action.Decline - result.content.shouldBeNull() - result.meta?.get("reason")?.jsonPrimitive?.content shouldBe "User skipped" + assertEquals(ElicitResult.Action.Decline, result.action) + assertNull(result.content) + assertEquals("User skipped", result.meta?.get("reason")?.jsonPrimitive?.content) } @Test fun `should require content only for accept action`() { - shouldThrow { + assertFailsWith { 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 index e5797b9..3da2a8b 100644 --- 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 @@ -15,12 +15,13 @@ import kotlin.test.assertNull class InitializeTest { @Test + @Suppress("LongMethod") fun `should serialize InitializeRequest with capabilities and meta`() { val request = InitializeRequest( InitializeRequestParams( protocolVersion = "2024-11-05", capabilities = ClientCapabilities( - sampling = ClientCapabilities.Sampling(), + sampling = ClientCapabilities.sampling, roots = ClientCapabilities.Roots(listChanged = true), elicitation = ClientCapabilities.elicitation, experimental = buildJsonObject { 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 index 500b303..b55a441 100644 --- 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 @@ -310,161 +310,4 @@ class NotificationTest { """.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/PrimitiveSchemaTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaTest.kt deleted file mode 100644 index 904454f..0000000 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaTest.kt +++ /dev/null @@ -1,479 +0,0 @@ -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/RequestTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/RequestTest.kt index 6087b20..1d6da19 100644 --- 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 @@ -2,120 +2,20 @@ 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") }) 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 index b7bc5cf..696f322 100644 --- 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 @@ -1,13 +1,7 @@ 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 @@ -20,21 +14,6 @@ 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") @@ -87,7 +66,7 @@ class SamplingTest { fun `should serialize SamplingMessage`() { val message = SamplingMessage( role = Role.User, - content = listOf(TextContent(text = "Summarize the latest release.")), + content = TextContent(text = "Summarize the latest release."), ) verifySerialization( @@ -106,6 +85,7 @@ class SamplingTest { } @Test + @Suppress("LongMethod") fun `should serialize CreateMessageRequest with all fields`() { val request = CreateMessageRequest( CreateMessageRequestParams( @@ -113,11 +93,11 @@ class SamplingTest { messages = listOf( SamplingMessage( role = Role.User, - content = listOf(TextContent(text = "You are a helpful assistant.")), + content = TextContent(text = "You are a helpful assistant."), ), SamplingMessage( role = Role.User, - content = listOf(TextContent(text = "Provide a short summary.")), + content = TextContent(text = "Provide a short summary."), ), ), modelPreferences = ModelPreferences( @@ -222,7 +202,7 @@ class SamplingTest { val message = params.messages.first() assertEquals(Role.User, message.role) - val content = assertIs(message.content.single()) + val content = assertIs(message.content) assertEquals("Draft a project update.", content.text) val preferences = assertNotNull(params.modelPreferences) @@ -279,7 +259,7 @@ class SamplingTest { val result = verifyDeserialization(McpJson, json) assertEquals(Role.Assistant, result.role) - val text = assertIs(result.content.single()) + val text = assertIs(result.content) assertEquals("Summary complete.", text.text) assertEquals("gpt-4o", result.model) assertEquals(StopReason.StopSequence, result.stopReason) @@ -287,249 +267,4 @@ class SamplingTest { 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 deleted file mode 100644 index 14bd248..0000000 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/TasksTest.kt +++ /dev/null @@ -1,802 +0,0 @@ -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 index 358866a..4663fbd 100644 --- 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 @@ -37,6 +37,7 @@ class ToolsTest { } @Test + @Suppress("LongMethod") fun `should serialize Tool with annotations and schemas`() { val inputSchema = ToolSchema( properties = buildJsonObject { @@ -157,61 +158,6 @@ class ToolsTest { 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() @@ -319,57 +265,6 @@ class ToolsTest { 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( @@ -580,102 +475,6 @@ class ToolsTest { 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() 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 index 25d9ee6..e4a949f 100644 --- 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 @@ -4,10 +4,8 @@ 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.double import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.put import kotlin.test.Test @@ -33,7 +31,6 @@ class CapabilitiesDslTest { roots.shouldBeNull() elicitation.shouldBeNull() experimental.shouldBeNull() - extensions.shouldBeNull() } } @@ -42,36 +39,40 @@ class CapabilitiesDslTest { val request = buildInitializeRequest { protocolVersion = "2024-11-05" capabilities { - sampling(ClientCapabilities.Sampling(tools = EmptyJsonObject)) + sampling { + put("temperature", 0.7) + put("maxTokens", 1000) + put("topP", 0.95) + } roots(listChanged = true) - elicitation(ClientCapabilities.Elicitation(form = EmptyJsonObject, url = EmptyJsonObject)) + elicitation { + put("mode", "interactive") + put("timeout", 30) + put("retries", 3) + } 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 + get("temperature")?.jsonPrimitive?.double shouldBe 0.7 + get("maxTokens")?.jsonPrimitive?.content shouldBe "1000" + get("topP")?.jsonPrimitive?.double shouldBe 0.95 } roots shouldNotBeNull { listChanged shouldBe true } elicitation shouldNotBeNull { - form shouldBe EmptyJsonObject - url shouldBe EmptyJsonObject + get("mode")?.jsonPrimitive?.content shouldBe "interactive" + get("timeout")?.jsonPrimitive?.content shouldBe "30" + get("retries")?.jsonPrimitive?.content shouldBe "3" } experimental shouldNotBeNull { get("customFeature")?.jsonPrimitive?.content shouldBe "true" @@ -79,11 +80,6 @@ class CapabilitiesDslTest { 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" - } - } } } @@ -114,57 +110,13 @@ class CapabilitiesDslTest { 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 + sampling { put("temperature", 0.5) } + sampling { put("temperature", 0.9) } // Should overwrite experimental { put("feature", "v1") } experimental { put("feature", "v2") } // Should overwrite } @@ -173,7 +125,7 @@ class CapabilitiesDslTest { request.params.capabilities.shouldNotBeNull { sampling shouldNotBeNull { - tools shouldBe EmptyJsonObject + get("temperature")?.jsonPrimitive?.double shouldBe 0.9 } experimental shouldNotBeNull { get("feature")?.jsonPrimitive?.content shouldBe "v2" 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 index 4801d02..180a308 100644 --- 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 @@ -42,7 +42,7 @@ class ContentDslTest { } } - (request.params.messages[0].content.single() as TextContent).shouldNotBeNull { + (request.params.messages[0].content as TextContent).shouldNotBeNull { text shouldBe "Hello, world!" annotations.shouldBeNull() meta.shouldBeNull() @@ -71,7 +71,7 @@ class ContentDslTest { } } - (request.params.messages[0].content.single() as TextContent).shouldNotBeNull { + (request.params.messages[0].content as TextContent).shouldNotBeNull { text shouldBe "Analyze the quarterly sales report for Q4 2025" annotations shouldNotBeNull { audience shouldBe listOf(Role.User, Role.Assistant) @@ -98,7 +98,7 @@ class ContentDslTest { } } - (request.params.messages[0].content.single() as TextContent).text shouldBe "Hello 🌍! Ça va? Москва 北京 مرحبا" + (request.params.messages[0].content as TextContent).text shouldBe "Hello 🌍! Ça va? Москва 北京 مرحبا" } @Test @@ -112,7 +112,7 @@ class ContentDslTest { } } - (request.params.messages[0].content.single() as TextContent).text shouldBe "" + (request.params.messages[0].content as TextContent).text shouldBe "" } @Test @@ -144,7 +144,7 @@ class ContentDslTest { } } - (request.params.messages[0].content.single() as ImageContent).shouldNotBeNull { + (request.params.messages[0].content as ImageContent).shouldNotBeNull { data shouldBe "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" mimeType shouldBe "image/png" @@ -177,7 +177,7 @@ class ContentDslTest { } } - (request.params.messages[0].content.single() as ImageContent).shouldNotBeNull { + (request.params.messages[0].content as ImageContent).shouldNotBeNull { mimeType shouldBe "image/jpeg" annotations shouldNotBeNull { audience shouldBe listOf(Role.Assistant) @@ -208,7 +208,7 @@ class ContentDslTest { } } } - (request.params.messages[0].content.single() as ImageContent).mimeType shouldBe mime + (request.params.messages[0].content as ImageContent).mimeType shouldBe mime } } @@ -256,7 +256,7 @@ class ContentDslTest { } } - (request.params.messages[0].content.single() as AudioContent).shouldNotBeNull { + (request.params.messages[0].content as AudioContent).shouldNotBeNull { data shouldBe "UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAAABmYWN0BAAAAAAAAABkYXRhAAAAAA==" mimeType shouldBe "audio/wav" annotations.shouldBeNull() @@ -288,7 +288,7 @@ class ContentDslTest { } } - (request.params.messages[0].content.single() as AudioContent).shouldNotBeNull { + (request.params.messages[0].content as AudioContent).shouldNotBeNull { mimeType shouldBe "audio/mpeg" annotations shouldNotBeNull { audience shouldBe listOf(Role.User) @@ -349,7 +349,7 @@ class ContentDslTest { } } } - (requestMin.params.messages[0].content.single() as TextContent).annotations?.priority shouldBe 0.0 + (requestMin.params.messages[0].content as TextContent).annotations?.priority shouldBe 0.0 // Priority = 1.0 (maximum) val requestMax = buildCreateMessageRequest { @@ -361,7 +361,7 @@ class ContentDslTest { } } } - (requestMax.params.messages[0].content.single() as TextContent).annotations?.priority shouldBe 1.0 + (requestMax.params.messages[0].content as TextContent).annotations?.priority shouldBe 1.0 } @Test @@ -376,7 +376,7 @@ class ContentDslTest { } } - (request.params.messages[0].content.single() as TextContent).annotations shouldNotBeNull { + (request.params.messages[0].content 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 index 4a34bab..e49e4d2 100644 --- 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 @@ -2,11 +2,8 @@ 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 @@ -27,15 +24,14 @@ class ElicitationDslTest { } request.params.message shouldBe "Provide info" - val formParams = request.params.shouldBeInstanceOf() - formParams.requestedSchema.properties["email"].shouldBeInstanceOf() - formParams.requestedSchema.required shouldBe listOf("email") + request.params.requestedSchema.properties["email"] shouldBe buildJsonObject { put("type", "string") } + request.params.requestedSchema.required shouldBe listOf("email") } @Test fun `buildElicitRequest should support direct requestedSchema`() { val schema = ElicitRequestParams.RequestedSchema( - properties = mapOf("name" to StringSchema()), + properties = buildJsonObject { put("name", buildJsonObject { put("type", "string") }) }, required = listOf("name"), ) val request = buildElicitRequest { @@ -43,20 +39,19 @@ class ElicitationDslTest { requestedSchema(schema) } - val formParams = request.params.shouldBeInstanceOf() - formParams.requestedSchema shouldBe schema + request.params.requestedSchema shouldBe schema } @Test fun `ElicitRequestedSchemaBuilder should support direct properties assignment`() { + val props = buildJsonObject { put("key", "value") } val request = buildElicitRequest { message = "Test" requestedSchema { - properties(buildJsonObject { put("key", buildJsonObject { put("type", "string") }) }) + properties(props) } } - val formParams = request.params.shouldBeInstanceOf() - formParams.requestedSchema.properties["key"].shouldBeInstanceOf() + request.params.requestedSchema.properties shouldBe props } @Test 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 index da2a4cb..cb35115 100644 --- 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 @@ -5,10 +5,10 @@ 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.int import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.put import kotlin.test.Test @@ -20,17 +20,16 @@ class InitializeDslTest { val request = buildInitializeRequest { protocolVersion = "2024-11-05" capabilities { - sampling(ClientCapabilities.Sampling()) + sampling { + put("maxTokens", 100) + } roots(listChanged = true) - elicitation(ClientCapabilities.Elicitation(form = EmptyJsonObject)) + elicitation { + put("mode", "interactive") + } experimental { put("custom", true) } - extensions( - mapOf( - "io.modelcontextprotocol/ui" to EmptyJsonObject, - ), - ) } info( name = "TestClient", @@ -42,13 +41,10 @@ class InitializeDslTest { request.params.protocolVersion shouldBe "2024-11-05" request.params.capabilities.shouldNotBeNull { - sampling shouldNotBeNull { } + sampling?.get("maxTokens")?.jsonPrimitive?.int shouldBe 100 roots?.listChanged shouldBe true - elicitation?.form shouldBe EmptyJsonObject + elicitation?.get("mode")?.jsonPrimitive?.content shouldBe "interactive" experimental?.get("custom")?.jsonPrimitive?.content shouldBe "true" - extensions shouldNotBeNull { - get("io.modelcontextprotocol/ui") shouldBe EmptyJsonObject - } } request.params.clientInfo.shouldNotBeNull { name shouldBe "TestClient" @@ -74,30 +70,25 @@ class InitializeDslTest { } @Test - fun `capabilities DSL should support direct typed values`() { - val samplingValue = ClientCapabilities.Sampling(tools = EmptyJsonObject) - val elicitationValue = ClientCapabilities.Elicitation(form = EmptyJsonObject, url = EmptyJsonObject) + fun `capabilities DSL should support direct JsonObject values`() { + val samplingObj = buildJsonObject { put("key", "value") } + val elicitationObj = buildJsonObject { put("key", "value") } 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) + sampling(samplingObj) + elicitation(elicitationObj) experimental(experimentalObj) - extensions(extensionsMap) } info("Test", "1.0") } request.params.capabilities.shouldNotBeNull { - sampling shouldBe samplingValue - elicitation shouldBe elicitationValue + sampling shouldBe samplingObj + elicitation shouldBe elicitationObj experimental shouldBe experimentalObj - extensions shouldBe extensionsMap } } 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 index 98817c1..c53a400 100644 --- 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 @@ -28,6 +28,7 @@ import kotlin.test.Test @OptIn(ExperimentalMcpApi::class) class SamplingDslTest { @Test + @Suppress("LongMethod") fun `buildCreateMessageRequest should build with all fields`() { val request = buildCreateMessageRequest { maxTokens = 1000 @@ -70,7 +71,7 @@ class SamplingDslTest { request.params.stopSequences shouldBe listOf("STOP") request.params.messages shouldHaveSize 5 - (request.params.messages[2].content.single() as TextContent).shouldNotBeNull { + (request.params.messages[2].content as TextContent).shouldNotBeNull { text shouldBe "Text with annotations" annotations shouldNotBeNull { audience shouldBe listOf(Role.User) @@ -80,13 +81,13 @@ class SamplingDslTest { meta?.get("key")?.jsonPrimitive?.content shouldBe "value" } - (request.params.messages[3].content.single() as ImageContent).shouldNotBeNull { + (request.params.messages[3].content as ImageContent).shouldNotBeNull { data shouldBe "base64image" mimeType shouldBe "image/png" annotations?.priority shouldBe 0.5 } - (request.params.messages[4].content.single() as AudioContent).shouldNotBeNull { + (request.params.messages[4].content as AudioContent).shouldNotBeNull { data shouldBe "base64audio" mimeType shouldBe "audio/wav" } @@ -101,7 +102,7 @@ class SamplingDslTest { @Test fun `buildCreateMessageRequest should support direct assignments`() { - val messages = listOf(SamplingMessage(Role.User, listOf(TextContent("Hello")))) + val messages = listOf(SamplingMessage(Role.User, TextContent("Hello"))) val preferences = ModelPreferences(costPriority = 0.1) val request = buildCreateMessageRequest { maxTokens = 100 @@ -125,8 +126,8 @@ class SamplingDslTest { assistant(content) } } - request.params.messages[0].content.single() shouldBe content - request.params.messages[1].content.single() shouldBe content + request.params.messages[0].content shouldBe content + request.params.messages[1].content shouldBe content } @Test 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 deleted file mode 100644 index fe3fca4..0000000 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcherSecurityTest.kt +++ /dev/null @@ -1,221 +0,0 @@ -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 deleted file mode 100644 index 2f43b5e..0000000 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcherTest.kt +++ /dev/null @@ -1,236 +0,0 @@ -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 deleted file mode 100644 index fb8aaf4..0000000 --- a/kotlin-sdk-core/src/jsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.js.kt +++ /dev/null @@ -1,16 +0,0 @@ -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 35a1e98..d00c1ab 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,6 +3,5 @@ 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 deleted file mode 100644 index 0cde506..0000000 --- a/kotlin-sdk-core/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.jvm.kt +++ /dev/null @@ -1,14 +0,0 @@ -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/ProtocolConcurrencyJvmTest.kt b/kotlin-sdk-core/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolConcurrencyJvmTest.kt deleted file mode 100644 index 60658f8..0000000 --- a/kotlin-sdk-core/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolConcurrencyJvmTest.kt +++ /dev/null @@ -1,114 +0,0 @@ -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/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 deleted file mode 100644 index 623fdfc..0000000 --- a/kotlin-sdk-core/src/nativeMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.native.kt +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index 7ad7bce..0000000 --- a/kotlin-sdk-core/src/wasmJsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.wasmJs.kt +++ /dev/null @@ -1,4 +0,0 @@ -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/api/kotlin-sdk-server.api b/kotlin-sdk-server/api/kotlin-sdk-server.api index d8e4c7a..c0b0199 100644 --- a/kotlin-sdk-server/api/kotlin-sdk-server.api +++ b/kotlin-sdk-server/api/kotlin-sdk-server.api @@ -1,10 +1,8 @@ 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; @@ -14,7 +12,6 @@ public abstract interface class io/modelcontextprotocol/kotlin/sdk/server/Client 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; @@ -25,44 +22,26 @@ public abstract interface class io/modelcontextprotocol/kotlin/sdk/server/Client 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;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 mcp (Lio/ktor/server/application/Application;Lkotlin/jvm/functions/Function1;)V + public static final fun mcp (Lio/ktor/server/routing/Route;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V + public static final fun mcp (Lio/ktor/server/routing/Route;Lkotlin/jvm/functions/Function1;)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 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;)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;ILjava/lang/Object;)V } public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt : io/modelcontextprotocol/kotlin/sdk/server/Feature { @@ -118,27 +97,22 @@ public class io/modelcontextprotocol/kotlin/sdk/server/Server { 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/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/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 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;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;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)V public final fun addTools (Ljava/util/List;)V 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 connect (Lio/modelcontextprotocol/kotlin/sdk/shared/Transport;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; @@ -147,17 +121,16 @@ public class io/modelcontextprotocol/kotlin/sdk/server/Server { 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 onConnect (Lkotlin/jvm/functions/Function0;)V + public final fun onInitialized (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 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; @@ -168,13 +141,9 @@ public class io/modelcontextprotocol/kotlin/sdk/server/Server { } public final class io/modelcontextprotocol/kotlin/sdk/server/ServerOptions : io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptions { - 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 { @@ -183,9 +152,7 @@ public class io/modelcontextprotocol/kotlin/sdk/server/ServerSession : io/modelc 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 @@ -200,9 +167,7 @@ public class io/modelcontextprotocol/kotlin/sdk/server/ServerSession : io/modelc 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; @@ -211,8 +176,7 @@ public class io/modelcontextprotocol/kotlin/sdk/server/ServerSession : io/modelc } 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;J)V - public synthetic fun (Ljava/lang/String;Lio/ktor/server/sse/ServerSSESession;JILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Lio/ktor/server/sse/ServerSSESession;)V public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun getSessionId ()Ljava/lang/String; public final fun handleMessage (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -223,22 +187,11 @@ public final class io/modelcontextprotocol/kotlin/sdk/server/SseServerTransport 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/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 @@ -260,14 +213,13 @@ public final class io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServe } 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 synthetic fun (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/time/Duration;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/time/Duration;Lkotlin/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; } diff --git a/kotlin-sdk-server/build.gradle.kts b/kotlin-sdk-server/build.gradle.kts index 193eb90..3a0a398 100644 --- a/kotlin-sdk-server/build.gradle.kts +++ b/kotlin-sdk-server/build.gradle.kts @@ -1,6 +1,5 @@ plugins { id("mcp.multiplatform") - id("mcp.detekt") id("mcp.publishing") id("mcp.dokka") alias(libs.plugins.kotlinx.binary.compatibility.validator) @@ -13,9 +12,7 @@ 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) } } @@ -40,7 +37,7 @@ kotlin { implementation(libs.ktor.server.content.negotiation) implementation(libs.ktor.server.test.host) - implementation(libs.logback.classic) + runtimeOnly(libs.slf4j.simple) } } } diff --git a/kotlin-sdk-server/detekt-baseline-commonMainSourceSet.xml b/kotlin-sdk-server/detekt-baseline-commonMainSourceSet.xml new file mode 100644 index 0000000..d383ec9 --- /dev/null +++ b/kotlin-sdk-server/detekt-baseline-commonMainSourceSet.xml @@ -0,0 +1,13 @@ + + + + + MagicNumber:StdioServerTransport.kt:StdioServerTransport$8192 + MaxLineLength:SSEServerTransport.kt:SseServerTransport$"SSEServerTransport already started! If using Server class, note that connect() calls start() automatically." + MaxLineLength:SSEServerTransport.kt:SseServerTransport$* + ReturnCount:KtorServer.kt:private suspend fun existingStreamableTransport: StreamableHttpServerTransport? + TooGenericExceptionCaught:SSEServerTransport.kt:SseServerTransport$e: Exception + TooGenericExceptionCaught:StdioServerTransport.kt:StdioServerTransport$e: Throwable + TooManyFunctions:KtorServer.kt:io.modelcontextprotocol.kotlin.sdk.server.KtorServer.kt + + diff --git a/kotlin-sdk-server/detekt-baseline-main.xml b/kotlin-sdk-server/detekt-baseline-main.xml index aad11fa..141c7d0 100644 --- a/kotlin-sdk-server/detekt-baseline-main.xml +++ b/kotlin-sdk-server/detekt-baseline-main.xml @@ -2,6 +2,28 @@ + InjectDispatcher:FeatureNotificationService.kt:FeatureNotificationService$Default + LongParameterList:KtorServer.kt:private suspend fun RoutingContext.streamableTransport: StreamableHttpServerTransport? + MagicNumber:StdioServerTransport.kt:StdioServerTransport$8192 + MaxLineLength:SSEServerTransport.kt:SseServerTransport$"SSEServerTransport already started! If using Server class, note that connect() calls start() automatically." + MaxLineLength:SSEServerTransport.kt:SseServerTransport$* + NoNameShadowing:FeatureNotificationService.kt:FeatureNotificationService${ it.remove(job) } + ReturnCount:KtorServer.kt:private suspend fun existingStreamableTransport: StreamableHttpServerTransport? + TooGenericExceptionCaught:SSEServerTransport.kt:SseServerTransport$e: Exception + TooGenericExceptionCaught:StdioServerTransport.kt:StdioServerTransport$e: Throwable + TooManyFunctions:KtorServer.kt:io.modelcontextprotocol.kotlin.sdk.server.KtorServer.kt UnsafeCallOnNullableType:StreamableHttpServerTransport.kt:StreamableHttpServerTransport$responseRequestId!! + UseCheckNotNull:Server.kt:Server$check(options.capabilities.prompts != null) { logger.error { "Failed to add prompt '${prompt.name}': Server does not support prompts capability" } "Server does not support prompts capability." } + UseCheckNotNull:Server.kt:Server$check(options.capabilities.prompts != null) { logger.error { "Failed to add prompts: Server does not support prompts capability" } "Server does not support prompts capability." } + UseCheckNotNull:Server.kt:Server$check(options.capabilities.prompts != null) { logger.error { "Failed to remove prompt '$name': Server does not support prompts capability" } "Server does not support prompts capability." } + UseCheckNotNull:Server.kt:Server$check(options.capabilities.prompts != null) { logger.error { "Failed to remove prompts: Server does not support prompts capability" } "Server does not support prompts capability." } + UseCheckNotNull:Server.kt:Server$check(options.capabilities.resources != null) { logger.error { "Failed to add resource '$name': Server does not support resources capability" } "Server does not support resources capability." } + UseCheckNotNull:Server.kt:Server$check(options.capabilities.resources != null) { logger.error { "Failed to add resources: Server does not support resources capability" } "Server does not support resources capability." } + UseCheckNotNull:Server.kt:Server$check(options.capabilities.resources != null) { logger.error { "Failed to remove resource '$uri': Server does not support resources capability" } "Server does not support resources capability." } + UseCheckNotNull:Server.kt:Server$check(options.capabilities.resources != null) { logger.error { "Failed to remove resources: Server does not support resources capability" } "Server does not support resources capability." } + UseCheckNotNull:Server.kt:Server$check(options.capabilities.tools != null) { logger.error { "Failed to add tool '${tool.name}': Server does not support tools capability" } "Server does not support tools capability. Enable it in ServerOptions." } + UseCheckNotNull:Server.kt:Server$check(options.capabilities.tools != null) { logger.error { "Failed to add tools: Server does not support tools capability" } "Server does not support tools capability." } + UseCheckNotNull:Server.kt:Server$check(options.capabilities.tools != null) { logger.error { "Failed to remove tool '$name': Server does not support tools capability" } "Server does not support tools capability." } + UseCheckNotNull:Server.kt:Server$check(options.capabilities.tools != null) { logger.error { "Failed to remove tools: Server does not support tools capability" } "Server does not support tools capability." } 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 index 21e0925..998d62c 100644 --- 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 @@ -5,19 +5,13 @@ 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 @@ -27,7 +21,6 @@ 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 {} @@ -36,6 +29,7 @@ private val logger = KotlinLogging.logger {} * communication through notifications, requests, and other operations. * This interface defines various methods to facilitate the interaction. */ +@Suppress("TooManyFunctions") public interface ClientConnection { /** @@ -97,17 +91,12 @@ public interface ClientConnection { * 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. + * @param requestedSchema The schema requested by the client 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, @@ -115,37 +104,14 @@ public interface ClientConnection { 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 @@ -179,15 +145,9 @@ public interface ClientConnection { * 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) } +@Suppress("TooManyFunctions") internal class ClientConnectionImpl(private val session: ServerSession) : ClientConnection { override val sessionId: String get() = session.sessionId @@ -212,34 +172,13 @@ internal class ClientConnectionImpl(private val session: ServerSession) : Client } 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." + with(request.params) { + logger.debug { + "Creating message with ${messages.size} messages, maxTokens=$maxTokens, " + + "temperature=$temperature, " + + "systemPrompt=${if (systemPrompt != null) "present" else "absent"}" } } - - 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) } @@ -250,31 +189,14 @@ internal class ClientConnectionImpl(private val session: ServerSession) : Client } 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}" + with(request.params) { + logger.debug { + "Creating elicitation with message length=${message.length}, " + + "schema properties count=${requestedSchema.properties.size}" } } 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 + return request(request, options) } override suspend fun createElicitation( @@ -282,19 +204,7 @@ internal class ClientConnectionImpl(private val session: ServerSession) : Client 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), - ), + request = ElicitRequest(params = ElicitRequestParams(message, requestedSchema)), options = options, ) @@ -322,14 +232,6 @@ internal class ClientConnectionImpl(private val session: ServerSession) : Client 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. 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 deleted file mode 100644 index d976d34..0000000 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ElicitationValidation.kt +++ /dev/null @@ -1,174 +0,0 @@ -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/Feature.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Feature.kt index a892c6c..609aaa2 100644 --- 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 @@ -8,9 +8,7 @@ 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 @@ -59,19 +57,3 @@ public data class RegisteredResource( ) : 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 index 42c2b8f..d996b2d 100644 --- 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 @@ -38,7 +38,6 @@ 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) @@ -341,7 +340,7 @@ internal class FeatureNotificationService( emitJobs.getAndUpdate { it.add(job) } // Register completion - job.invokeOnCompletion { _ -> + job.invokeOnCompletion { emitJobs.getAndUpdate { it.remove(job) } } 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 index 142d59c..0ed6558 100644 --- 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 @@ -20,12 +20,9 @@ internal interface FeatureListener { * 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. + * Used primarily for logging purposes. */ internal class FeatureRegistry(private val featureType: String) { @@ -47,11 +44,11 @@ internal class FeatureRegistry(private val featureType: String) { private val listeners = atomic(persistentListOf()) internal fun addListener(listener: FeatureListener) { - listeners.update { it.adding(listener) } + listeners.update { it.add(listener) } } internal fun removeListener(listener: FeatureListener) { - listeners.update { it.removing(listener) } + listeners.update { it.remove(listener) } } /** @@ -61,42 +58,27 @@ internal class FeatureRegistry(private val featureType: String) { */ 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) - } + val oldMap = registry.getAndUpdate { current -> current.put(feature.key, feature) } + val oldFeature = oldMap[feature.key] logger.info { "Added $featureType: \"${feature.key}\"" } - notifyFeatureUpdated(null, feature) + notifyFeatureUpdated(oldFeature, 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. + * and added to the current registry. * * @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) - } + val oldMap = registry.getAndUpdate { current -> current.putAll(features.associateBy { it.key }) } logger.info { "Added ${featureType}s: ${features.size}" } for (feature in features) { - notifyFeatureUpdated(null, feature) + val oldFeature = oldMap[feature.key] + notifyFeatureUpdated(oldFeature, feature) } } @@ -108,7 +90,7 @@ internal class FeatureRegistry(private val featureType: String) { */ internal fun remove(key: FeatureKey): Boolean { logger.info { "Removing $featureType: \"$key\"" } - val oldMap = registry.getAndUpdate { current -> current.removing(key) } + val oldMap = registry.getAndUpdate { current -> current.remove(key) } val removedFeature = oldMap[key] val removed = removedFeature != null 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 deleted file mode 100644 index ef010fe..0000000 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/HostValidation.kt +++ /dev/null @@ -1,195 +0,0 @@ -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 3127499..739f8d3 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,18 +1,15 @@ +@file:Suppress("TooManyFunctions") + 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.Route import io.ktor.server.routing.RoutingContext @@ -21,10 +18,8 @@ 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.utils.io.KtorDsl import io.modelcontextprotocol.kotlin.sdk.types.RPCError @@ -40,31 +35,14 @@ private val logger = KotlinLogging.logger {} * 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 -@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, -) { +public fun Route.mcp(path: String, block: ServerSSESession.() -> Server) { route(path) { - mcp(enableDnsRebindingProtection, allowedHosts, allowedOrigins, maxRequestBodySize, block) + mcp(block) } } @@ -75,28 +53,12 @@ public fun Route.mcp( * **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 -@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, -) { +public fun Route.mcp(block: ServerSSESession.() -> Server) { try { plugin(SSE) } catch (e: MissingApplicationPluginException) { @@ -108,12 +70,10 @@ public fun Route.mcp( ) } - installDnsRebindingProtection(enableDnsRebindingProtection, allowedHosts, allowedOrigins) - val transportManager = TransportManager() sse { - mcpSseEndpoint("", transportManager, maxRequestBodySize, block) + mcpSseEndpoint("", transportManager, block) } post { @@ -126,70 +86,31 @@ public fun Route.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 -@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() +public fun Application.mcp(block: ServerSSESession.() -> Server) { install(SSE) routing { - mcp(enableDnsRebindingProtection, allowedHosts, allowedOrigins, maxRequestBodySize, block) + mcp(block) } } -@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) } @@ -218,63 +139,48 @@ private fun Application.mcpStreamableHttp( * 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 enableDnsRebindingProtection Enables DNS rebinding attack protection for the endpoint. Defaults to false. + * @param allowedHosts A list of hostnames allowed to access the endpoint. If `null`, no restrictions are applied. + * @param allowedOrigins A list of origins allowed to perform cross-origin requests (CORS). + * If `null`, no restrictions are applied. * @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 +@Suppress("LongParameterList") public fun Application.mcpStreamableHttp( path: String = "/mcp", - enableDnsRebindingProtection: Boolean = true, + enableDnsRebindingProtection: Boolean = false, 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( + enableDnsRebindingProtection = enableDnsRebindingProtection, + allowedHosts = allowedHosts, + allowedOrigins = allowedOrigins, 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() + install(SSE) routing { route(path) { - installDnsRebindingProtection(enableDnsRebindingProtection, allowedHosts, allowedOrigins) - post { mcpStatelessStreamableHttpEndpoint( configuration = configuration, @@ -282,10 +188,18 @@ private fun Application.mcpStatelessStreamableHttp( ) } get { - call.rejectUnsupportedMethod() + call.reject( + HttpStatusCode.MethodNotAllowed, + RPCError.ErrorCode.CONNECTION_CLOSED, + "Method not allowed.", + ) } delete { - call.rejectUnsupportedMethod() + call.reject( + HttpStatusCode.MethodNotAllowed, + RPCError.ErrorCode.CONNECTION_CLOSED, + "Method not allowed.", + ) } } } @@ -296,79 +210,45 @@ private fun Application.mcpStatelessStreamableHttp( * 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]. + * Simple request/response pairs are returned as JSON (not SSE streams). * * @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 enableDnsRebindingProtection Determines whether DNS rebinding protection is enabled. Defaults to `false`. + * @param allowedHosts A list of allowed hostnames. If null, host filtering is disabled. + * @param allowedOrigins A list of allowed origins for CORS. If null, origin filtering is disabled. + * @param eventStore An optional [EventStore] implementation to provide resumability and event replay support. * @param block factory block with access to the [RoutingContext] (for reading request headers) * that creates and returns the [Server] to handle the connection. */ @KtorDsl +@Suppress("LongParameterList") public fun Application.mcpStatelessStreamableHttp( path: String = "/mcp", - enableDnsRebindingProtection: Boolean = true, + enableDnsRebindingProtection: Boolean = false, allowedHosts: List? = null, allowedOrigins: List? = null, + eventStore: EventStore? = null, block: RoutingContext.() -> Server, ) { mcpStatelessStreamableHttp( path = path, - enableDnsRebindingProtection = enableDnsRebindingProtection, - allowedHosts = allowedHosts, - allowedOrigins = allowedOrigins, configuration = StreamableHttpServerTransport.Configuration( + enableDnsRebindingProtection = enableDnsRebindingProtection, + allowedHosts = allowedHosts, + allowedOrigins = allowedOrigins, + eventStore = eventStore, 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, transportManager: TransportManager, - maxRequestBodySize: Long, block: ServerSSESession.() -> Server, ) { - val transport = mcpSseTransport(postEndpoint, transportManager, maxRequestBodySize) + val transport = mcpSseTransport(postEndpoint, transportManager) val server = block() @@ -387,9 +267,8 @@ private suspend fun ServerSSESession.mcpSseEndpoint( private fun ServerSSESession.mcpSseTransport( postEndpoint: String, transportManager: TransportManager, - maxRequestBodySize: Long, ): SseServerTransport { - val transport = SseServerTransport(postEndpoint, this, maxRequestBodySize) + val transport = SseServerTransport(postEndpoint, this) transportManager.addTransport(transport.sessionId, transport) logger.info { "New SSE connection established and stored with sessionId: ${transport.sessionId}" } @@ -407,17 +286,11 @@ private suspend fun RoutingContext.mcpStatelessStreamableHttpEndpoint( logger.info { "New stateless StreamableHttp connection established without sessionId" } val server = block() - val session = server.createSession(transport) + server.onClose { logger.info { "Server connection closed without sessionId" } } + 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" } - } + transport.handleRequest(null, this.call) + logger.debug { "Server connected to transport without sessionId" } } private suspend fun RoutingContext.mcpPostEndpoint(transportManager: TransportManager) { @@ -439,16 +312,6 @@ private suspend fun RoutingContext.mcpPostEndpoint(transportManager: TransportMa logger.trace { "Message handled for sessionId: $sessionId" } } -/** A stateless endpoint serves POST only, offering neither an SSE stream to open nor a session to delete. */ -private suspend fun ApplicationCall.rejectUnsupportedMethod() { - response.header(HttpHeaders.Allow, HttpMethod.Post.value) - reject( - HttpStatusCode.MethodNotAllowed, - RPCError.ErrorCode.CONNECTION_CLOSED, - "Method not allowed.", - ) -} - private fun ApplicationRequest.sessionId(): String? = header(MCP_SESSION_ID_HEADER) private suspend fun existingStreamableTransport( @@ -510,14 +373,3 @@ private suspend fun RoutingContext.streamableTransport( 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 deleted file mode 100644 index 42093c8..0000000 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServerHelpers.kt +++ /dev/null @@ -1,46 +0,0 @@ -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 deleted file mode 100644 index 0cc4143..0000000 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/RequestBody.kt +++ /dev/null @@ -1,34 +0,0 @@ -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 f525ec2..619266a 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,6 +5,7 @@ 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.shared.AbstractTransport @@ -25,25 +26,12 @@ internal const val SESSION_ID_PARAM = "sessionId" * 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, - private val maxRequestBodySize: Long = DEFAULT_MAX_REQUEST_BODY_SIZE, -) : AbstractTransport() { - init { - require(maxRequestBodySize > 0) { "maxRequestBodySize must be greater than 0" } - } - +public class SseServerTransport(private val endpoint: String, private val session: ServerSSESession) : + AbstractTransport() { 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() @@ -85,7 +73,6 @@ public class SseServerTransport( val message = "SSE connection not established" call.respondText(message, status = HttpStatusCode.InternalServerError) _onError.invoke(IllegalStateException(message)) - return } val body = try { @@ -94,13 +81,7 @@ public class SseServerTransport( error("Unsupported content-type: $ct") } - 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 + call.receiveText() } catch (e: Exception) { call.respondText("Invalid message: ${e.message}", status = HttpStatusCode.BadRequest) _onError.invoke(e) @@ -109,8 +90,6 @@ public class SseServerTransport( try { handleMessage(body) - } catch (e: CancellationException) { - throw e } catch (e: Exception) { call.respondText("Error handling message $body: ${e.message}", status = HttpStatusCode.BadRequest) return @@ -127,8 +106,6 @@ public class SseServerTransport( try { val parsedMessage = McpJson.decodeFromString(message) _onMessage.invoke(parsedMessage) - } catch (e: CancellationException) { - throw e } catch (e: Exception) { _onError.invoke(e) throw e 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 deleted file mode 100644 index 8243171..0000000 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingValidation.kt +++ /dev/null @@ -1,54 +0,0 @@ -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 6f7711f..2401709 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,7 +1,6 @@ package io.modelcontextprotocol.kotlin.sdk.server import io.github.oshai.kotlinlogging.KotlinLogging -import io.modelcontextprotocol.kotlin.sdk.server.utils.warnIfInvalidToolName import io.modelcontextprotocol.kotlin.sdk.shared.ProtocolOptions import io.modelcontextprotocol.kotlin.sdk.shared.RequestOptions import io.modelcontextprotocol.kotlin.sdk.shared.Transport @@ -11,7 +10,6 @@ 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 @@ -27,38 +25,24 @@ 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 {} @@ -67,27 +51,10 @@ private val logger = KotlinLogging.logger {} * Configuration options for the MCP server. * * @property capabilities The capabilities this server supports. - * @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]. + * @property enforceStrictCapabilities Whether to strictly enforce capabilities when interacting with clients. */ -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) -} +public class ServerOptions(public val capabilities: ServerCapabilities, enforceStrictCapabilities: Boolean = true) : + ProtocolOptions(enforceStrictCapabilities = enforceStrictCapabilities) /** * An MCP server is responsible for storing features and handling new connections. @@ -101,12 +68,13 @@ public class ServerOptions( * 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 + * @param serverInfo Information about this server implementation (name, version). + * @param options Configuration options for the server. + * @param 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. */ +@Suppress("TooManyFunctions") public open class Server( protected val serverInfo: Implementation, protected val options: ServerOptions, @@ -128,6 +96,8 @@ public open class Server( block: Server.() -> Unit = {}, ) : this(serverInfo, options, { instructions }, block) + private var _onInitialized: (() -> Unit) = {} + private var _onConnect: (() -> Unit) = {} private var _onClose: () -> Unit = {} @@ -155,11 +125,6 @@ public open class Server( addListener(notificationService.resourceUpdatedListener) } } - private val resourceTemplateRegistry = FeatureRegistry("ResourceTemplate").apply { - if (options.capabilities.resources?.listChanged == true) { - addListener(notificationService.resourceListChangedListener) - } - } /** * Provides a snapshot of all sessions currently registered in the server @@ -185,17 +150,10 @@ public open class 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() @@ -206,6 +164,20 @@ public open class Server( _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. + */ + @Deprecated( + "Use createSession(transport) instead.", + ReplaceWith("createSession(transport)"), + DeprecationLevel.ERROR, + ) + public suspend fun connect(transport: Transport): ServerSession = createSession(transport) + /** * Starts a new server session with the given transport and initializes * internal request handlers based on the server's capabilities. @@ -289,6 +261,22 @@ public open class Server( } } + /** + * Registers a callback to be invoked when the server has completed initialization. + */ + @Deprecated( + "Initialization moved to ServerSession, use ServerSession.onInitialized instead.", + ReplaceWith("ServerSession.onInitialized"), + DeprecationLevel.ERROR, + ) + public fun onInitialized(block: () -> Unit) { + val old = _onInitialized + _onInitialized = { + old() + block() + } + } + /** * Registers a callback to be invoked when the server connection is closing. */ @@ -308,11 +296,11 @@ public open class Server( * @throws IllegalStateException If the server does not support tools. */ public fun addTool(tool: Tool, handler: suspend ClientConnection.(CallToolRequest) -> CallToolResult) { - checkNotNull(options.capabilities.tools) { + check(options.capabilities.tools != null) { logger.error { "Failed to add tool '${tool.name}': Server does not support tools capability" } "Server does not support tools capability. Enable it in ServerOptions." } - warnIfInvalidToolName(tool.name) + toolRegistry.add(RegisteredTool(tool, handler)) } @@ -320,16 +308,16 @@ public open class Server( * Registers a single tool. The client can then call this tool. * * @param name The name of the tool. + * @param title An optional human-readable name of the tool for display purposes. * @param description A human-readable description of what the tool does. * @param inputSchema The expected input schema for the tool. - * @param 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. */ + @Suppress("LongParameterList") public fun addTool( name: String, description: String, @@ -337,7 +325,6 @@ public open class Server( title: String? = null, outputSchema: ToolSchema? = null, toolAnnotations: ToolAnnotations? = null, - execution: ToolExecution? = null, meta: JsonObject? = null, handler: suspend ClientConnection.(CallToolRequest) -> CallToolResult, ) { @@ -348,7 +335,6 @@ public open class Server( description = description, title = title, annotations = toolAnnotations, - execution = execution, meta = meta, ) addTool(tool, handler) @@ -361,11 +347,10 @@ public open class Server( * @throws IllegalStateException If the server does not support tools. */ public fun addTools(toolsToAdd: List) { - checkNotNull(options.capabilities.tools) { + check(options.capabilities.tools != null) { logger.error { "Failed to add tools: Server does not support tools capability" } "Server does not support tools capability." } - toolsToAdd.forEach { warnIfInvalidToolName(it.tool.name) } toolRegistry.addAll(toolsToAdd) } @@ -377,7 +362,7 @@ public open class Server( * @throws IllegalStateException If the server does not support tools. */ public fun removeTool(name: String): Boolean { - checkNotNull(options.capabilities.tools) { + check(options.capabilities.tools != null) { logger.error { "Failed to remove tool '$name': Server does not support tools capability" } "Server does not support tools capability." } @@ -392,7 +377,7 @@ public open class Server( * @throws IllegalStateException If the server does not support tools. */ public fun removeTools(toolNames: List): Int { - checkNotNull(options.capabilities.tools) { + check(options.capabilities.tools != null) { logger.error { "Failed to remove tools: Server does not support tools capability" } "Server does not support tools capability." } @@ -412,7 +397,7 @@ public open class Server( prompt: Prompt, promptProvider: suspend ClientConnection.(GetPromptRequest) -> GetPromptResult, ) { - checkNotNull(options.capabilities.prompts) { + check(options.capabilities.prompts != null) { logger.error { "Failed to add prompt '${prompt.name}': Server does not support prompts capability" } "Server does not support prompts capability." } @@ -445,7 +430,7 @@ public open class Server( * @throws IllegalStateException If the server does not support prompts. */ public fun addPrompts(promptsToAdd: List) { - checkNotNull(options.capabilities.prompts) { + check(options.capabilities.prompts != null) { logger.error { "Failed to add prompts: Server does not support prompts capability" } "Server does not support prompts capability." } @@ -460,7 +445,7 @@ public open class Server( * @throws IllegalStateException If the server does not support prompts. */ public fun removePrompt(name: String): Boolean { - checkNotNull(options.capabilities.prompts) { + check(options.capabilities.prompts != null) { logger.error { "Failed to remove prompt '$name': Server does not support prompts capability" } "Server does not support prompts capability." } @@ -476,7 +461,7 @@ public open class Server( * @throws IllegalStateException If the server does not support prompts. */ public fun removePrompts(promptNames: List): Int { - checkNotNull(options.capabilities.prompts) { + check(options.capabilities.prompts != null) { logger.error { "Failed to remove prompts: Server does not support prompts capability" } "Server does not support prompts capability." } @@ -501,7 +486,7 @@ public open class Server( mimeType: String = "text/html", readHandler: suspend ClientConnection.(ReadResourceRequest) -> ReadResourceResult, ) { - checkNotNull(options.capabilities.resources) { + check(options.capabilities.resources != null) { logger.error { "Failed to add resource '$name': Server does not support resources capability" } "Server does not support resources capability." } @@ -516,7 +501,7 @@ public open class Server( * @throws IllegalStateException If the server does not support resources. */ public fun addResources(resourcesToAdd: List) { - checkNotNull(options.capabilities.resources) { + check(options.capabilities.resources != null) { logger.error { "Failed to add resources: Server does not support resources capability" } "Server does not support resources capability." } @@ -531,7 +516,7 @@ public open class Server( * @throws IllegalStateException If the server does not support resources. */ public fun removeResource(uri: String): Boolean { - checkNotNull(options.capabilities.resources) { + check(options.capabilities.resources != null) { logger.error { "Failed to remove resource '$uri': Server does not support resources capability" } "Server does not support resources capability." } @@ -546,74 +531,13 @@ public open class Server( * @throws IllegalStateException If the server does not support resources. */ public fun removeResources(uris: List): Int { - checkNotNull(options.capabilities.resources) { + check(options.capabilities.resources != null) { logger.error { "Failed to remove resources: Server does not support resources capability" } "Server does not support resources capability." } return resourceRegistry.removeAll(uris) } - /** - * Registers a resource template. Clients can discover it via `resources/templates/list` - * and read matching URIs via `resources/read`. - * - * @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 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, - ), - ) - } - - /** - * 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) { @@ -652,6 +576,7 @@ public open class Server( } // Execute the tool handler and catch any errors + @Suppress("TooGenericExceptionCaught") return try { logger.trace { "Executing tool ${requestParams.name} with input: ${requestParams.arguments}" } tool.run { @@ -659,9 +584,6 @@ public open class Server( } } 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( @@ -682,10 +604,7 @@ public open class Server( 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}", - ) + throw IllegalArgumentException("Prompt not found: ${requestParams.name}") } return prompt.run { session.clientConnection.messageProvider(request) @@ -698,43 +617,25 @@ public open class Server( } 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 }, - ) + val requestParams = request.params + logger.debug { "Handling read resource request for: ${requestParams.uri}" } + val resource = resourceRegistry.get(requestParams.uri) ?: 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.error { "Resource not found: ${requestParams.uri}" } + throw IllegalArgumentException("Resource not found: ${requestParams.uri}") } - - logger.debug { "Matched resource template '${template.key}' for URI: $uri" } - return template.run { session.clientConnection.readHandler(request, matchResult.variables) } + return resource.run { + session.clientConnection.readHandler(request) + } } private fun handleListResourceTemplates(): ListResourceTemplatesResult { - logger.debug { "Handling list resource templates request" } - return ListResourceTemplatesResult(resourceTemplateRegistry.values.values.map { it.resourceTemplate }) + // If you have resource templates, return them here. For now, return empty. + return ListResourceTemplatesResult(listOf()) } // 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 @@ -795,30 +696,6 @@ public open class Server( options = options, ) - /** - * Triggers URL mode [ClientConnection.createElicitation] request for session by provided [sessionId]. - * - * @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 createElicitation( - sessionId: String, - message: String, - elicitationId: String, - url: String, - options: RequestOptions? = null, - ): ElicitResult = clientConnection(sessionId).createElicitation( - message = message, - elicitationId = elicitationId, - url = url, - options = options, - ) - /** * Triggers [ClientConnection.sendLoggingMessage] for session by provided [sessionId]. * @@ -865,35 +742,21 @@ public open class Server( 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) } } - /** Removes the notification handler for the given [method] from all active sessions. */ public fun removeNotificationHandler(method: Method) { sessions.forEach { (_, session) -> session.removeNotificationHandler(method) } } - /** Registers a notification [handler] for the given [method] on a specific session. */ public fun setNotificationHandler( sessionId: String, method: Method, @@ -902,7 +765,6 @@ public open class Server( 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) } 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 index f20d3e1..69d20d1 100644 --- 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 @@ -9,7 +9,6 @@ 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 @@ -21,10 +20,8 @@ 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 @@ -39,28 +36,15 @@ 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 + * Represents a server session. */ +@Suppress("TooManyFunctions") 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() @@ -68,14 +52,17 @@ public open class ServerSession( private var _onClose: () -> Unit = {} - private val _clientCapabilities: AtomicRef = atomic(null) - private val _clientVersion: AtomicRef = atomic(null) + /** + * The client's reported capabilities after initialization. + */ + public var clientCapabilities: ClientCapabilities? = null + private set - /** 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 client's version information after initialization. + */ + public var clientVersion: Implementation? = null + private set /** * The capabilities supported by the server, related to the session. @@ -108,18 +95,10 @@ public open class ServerSession( } } - 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 @@ -180,12 +159,6 @@ public open class ServerSession( } } - Defined.TasksGet, - Defined.TasksResult, - Defined.TasksList, - Defined.TasksCancel, - -> assertTasksCapabilityForMethod(method) - Defined.Ping -> { // No specific capability required } @@ -196,28 +169,6 @@ public open class ServerSession( } } - 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. * @@ -261,12 +212,6 @@ public open class ServerSession( } } - Defined.NotificationsTasksStatus -> { - if (serverCapabilities.tasks == null) { - error("Server does not support tasks (required for ${method.value})") - } - } - Defined.NotificationsCancelled, Defined.NotificationsProgress, -> { @@ -341,15 +286,9 @@ public open class ServerSession( } 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 + clientCapabilities = request.params.capabilities + clientVersion = request.params.clientInfo val requestedVersion = request.params.protocolVersion val protocolVersion = if (SUPPORTED_PROTOCOL_VERSIONS.contains(requestedVersion)) { @@ -421,17 +360,12 @@ public open class ServerSession( * 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. + * @param requestedSchema The schema requested by the client 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, @@ -439,24 +373,6 @@ public open class ServerSession( 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. @@ -489,13 +405,5 @@ public open class ServerSession( * 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/StdioServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt index 9261be2..8fd46dd 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 @@ -4,27 +4,16 @@ import io.github.oshai.kotlinlogging.KotlinLogging 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 @@ -34,347 +23,195 @@ import kotlinx.io.Source import kotlinx.io.buffered import kotlinx.io.readByteArray import kotlinx.io.writeString -import kotlin.concurrent.atomics.AtomicReference +import kotlin.concurrent.atomics.AtomicBoolean import kotlin.concurrent.atomics.ExperimentalAtomicApi +import kotlin.coroutines.CoroutineContext private const val READ_BUFFER_SIZE = 8192L /** - * Server-side MCP transport that exchanges JSON-RPC messages over arbitrary byte streams. + * A server transport that communicates with a client via standard I/O. * - * Reads framed messages from the supplied [Source] and writes framed messages to the supplied - * [Sink]. Three internal coroutines drive the pipeline: + * Reads from input [Source] and writes to output [Sink]. * - * - **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() - * ``` + * @constructor Creates a new instance of [StdioServerTransport]. + * @param inputStream The input [Source] used to receive data. + * @param outputStream The output [Sink] used to send data. */ @OptIn(ExperimentalAtomicApi::class) -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), - ) +public class StdioServerTransport(private val inputStream: Source, outputStream: Sink) : AbstractTransport() { private val logger = KotlinLogging.logger {} private val readBuffer = ReadBuffer() - private val readChannel = Channel(Channel.BUFFERED) - private val writeChannel = Channel(Channel.BUFFERED) - private val outputSink = output.buffered() + private val initialized: AtomicBoolean = AtomicBoolean(false) + private var readingJob: Job? = null + private var sendingJob: Job? = null + private var processingJob: Job? = null - private enum class State { New, Operational, Stopped } + 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 outputSink = outputStream.buffered() - 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 (!state.compareAndSet(State.New, State.Operational)) { - when (state.load()) { - State.Stopped -> error("StdioServerTransport is already closed!") - else -> error("StdioServerTransport already started!") - } + if (!initialized.compareAndSet(expectedValue = false, newValue = true)) { + error("StdioServerTransport already started!") } - try { - val resolvedScope = scope ?: CoroutineScope( - currentCoroutineContext() + IODispatcher + SupervisorJob(), - ) - effectiveScope = resolvedScope + // Launch a coroutine to read from stdin + readingJob = launchReadingJob() - 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) - } + // Launch a coroutine to process messages from readChannel + processingJob = launchProcessingJob() + + // Launch a coroutine to handle message sending + sendingJob = launchSendingJob() } - 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) - } - } - } 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) + private fun launchReadingJob(): Job { + val job = scope.launch { + val buf = Buffer() + @Suppress("TooGenericExceptionCaught") + try { + while (isActive) { + val bytesRead = inputStream.readAtMostTo(buf, READ_BUFFER_SIZE) + if (bytesRead == -1L) { + // EOF reached + break + } + if (bytesRead > 0) { + val chunk = buf.readByteArray() + readChannel.send(chunk) } } + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + logger.error(e) { "Error reading from stdin" } + _onError.invoke(e) + } finally { + // Reached EOF or error, close connection + close() + } + } + job.invokeOnCompletion { cause -> + logJobCompletion("Message reading", cause) + } + return job + } + + private fun launchProcessingJob(): Job { + val job = scope.launch { + @Suppress("TooGenericExceptionCaught") + try { + for (chunk in readChannel) { + readBuffer.append(chunk) + processReadBuffer() + } + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + _onError.invoke(e) + } + } + job.invokeOnCompletion { cause -> + logJobCompletion("Processing", cause) + } + return job + } + + private fun launchSendingJob(): Job { + val job = scope.launch { + @Suppress("TooGenericExceptionCaught") + 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 stdout" } + _onError.invoke(e) + } + } + job.invokeOnCompletion { cause -> + logJobCompletion("Message sending", cause) + if (cause is CancellationException) { + readingJob?.cancel(cause) + } + } + return job + } + + private suspend fun processReadBuffer() { + @Suppress("TooGenericExceptionCaught") + while (true) { + val message = try { + readBuffer.readMessage() + } catch (e: Throwable) { + _onError.invoke(e) + null + } + + if (message == null) break + // Async invocation broke delivery order + try { + _onMessage.invoke(message) + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + logger.error(e) { "Error processing message" } + _onError.invoke(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() + private fun logJobCompletion(jobName: String, cause: Throwable?) { + when (cause) { + is CancellationException -> { + } + + null -> { + logger.debug { "$jobName job completed" } + } + + else -> { + logger.debug(cause) { "$jobName job completed exceptionally" } } - } 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() { - var previous: State - do { - previous = state.load() - } while (previous != State.Stopped && !state.compareAndSet(previous, State.Stopped)) - - if (previous == State.New) { - setupComplete.complete(Unit) - return - } + if (!initialized.compareAndSet(expectedValue = true, newValue = false)) return withContext(NonCancellable) { - setupComplete.await() - - 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" } } + sendingJob?.cancelAndJoin() + + runCatching { + inputStream.close() + }.onFailure { logger.warn(it) { "Failed to close stdin" } } + + readingJob?.cancel() + readChannel.close() + + processingJob?.cancelAndJoin() + readBuffer.clear() - if (ownsScope) { - effectiveScope?.coroutineContext?.get(Job)?.cancel() - } + + runCatching { + outputSink.flush() + outputSink.close() + }.onFailure { logger.warn(it) { "Failed to close stdout" } } + invokeOnCloseCallback() } } - /** - * 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) + writeChannel.send(message) } } 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 index dcba848..580fcc0 100644 --- 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 @@ -1,28 +1,25 @@ 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.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.respondNullable 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 @@ -31,19 +28,14 @@ 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 @@ -52,7 +44,7 @@ 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" +private const val MAXIMUM_MESSAGE_SIZE = 4 * 1024 * 1024 // 4 MB /** * A holder for an active request call. @@ -79,6 +71,7 @@ private data class SessionContext(val session: ServerSSESession?, val call: Appl * @param configuration Transport configuration. See [Configuration] for available options. */ @OptIn(ExperimentalUuidApi::class, ExperimentalAtomicApi::class) +@Suppress("TooManyFunctions") public class StreamableHttpServerTransport(private val configuration: Configuration) : AbstractTransport() { @Deprecated("Use default constructor with explicit Configuration()") @@ -101,6 +94,7 @@ public class StreamableHttpServerTransport(private val configuration: Configurat * @param retryIntervalMillis Retry interval in milliseconds for event handling or reconnection attempts. * Defaults to `null`. */ + @Suppress("MaxLineLength") @Deprecated( "Use constructor with Configuration: StreamableHttpServerTransport(Configuration(enableJsonResponse = ...))", replaceWith = ReplaceWith( @@ -126,46 +120,35 @@ public class StreamableHttpServerTransport(private val configuration: Configurat ) /** - * Configuration options for [StreamableHttpServerTransport]. + * Configuration for managing various aspects of the 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). + * @property enableJsonResponse Determines whether the server should return JSON responses. + * Defaults to `false`. + * + * @property enableDnsRebindingProtection Enables DNS rebinding protection. + * Defaults to `false`. + * + * @property allowedHosts A list of hosts allowed for server communication. + * Defaults to `null`, allowing all hosts. + * + * @property allowedOrigins A list of allowed origins for CORS (Cross-Origin Resource Sharing). + * Defaults to `null`, allowing all origins. + * + * @property eventStore The `EventStore` instance for handling resumable events. + * Defaults to `null`, disabling resumability. + * + * @property retryInterval Retry interval for event handling or reconnection attempts. + * Defaults to `null`. */ 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 @@ -180,20 +163,6 @@ public class StreamableHttpServerTransport(private val configuration: Configurat 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() @@ -242,6 +211,7 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } } + @Suppress("CyclomaticComplexMethod", "ReturnCount") override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { val responseRequestId: RequestId? = when (message) { is JSONRPCResponse -> message.id @@ -260,148 +230,66 @@ public class StreamableHttpServerTransport(private val configuration: Configurat 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") - } + ?: 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 } + val isTerminated = message is JSONRPCResponse || message is JSONRPCError + if (!isTerminated) return + requestToResponseMapping[responseRequestId!!] = message + val relatedIds = requestToStreamMapping.filterValues { it == streamId }.keys + + if (relatedIds.any { it !in requestToResponseMapping }) return 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 (activeStream == null) error("No connection established for request ID: $routingRequestId") 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 - } + activeStream.call.response.header(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + sessionId?.let { activeStream.call.response.header(MCP_SESSION_ID_HEADER, it) } val responses = relatedIds.mapNotNull { requestToResponseMapping[it] } - cleanUpRequests(relatedIds) - pendingPost.complete(responses) + val payload = if (responses.size == 1) { + responses.first() + } else { + responses + } + activeStream.call.respond(payload) } 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) + activeStream.session?.close() } - } - } - 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() + // Clean up + relatedIds.forEach { requestId -> + requestToResponseMapping.remove(requestId) + requestToStreamMapping.remove(requestId) } - .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) { - } + 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() } + streamsMapping.clear() + requestToStreamMapping.clear() + requestToResponseMapping.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. + * Handles an incoming HTTP request, whether GET, POST or DELETE */ public suspend fun handleRequest(session: ServerSSESession?, call: ApplicationCall) { validateHeaders(call)?.let { reason -> @@ -425,13 +313,9 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } /** - * 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. + * Handles POST requests containing JSON-RPC messages */ + @Suppress("CyclomaticComplexMethod", "LongMethod", "ReturnCount", "TooGenericExceptionCaught") public suspend fun handlePostRequest(session: ServerSSESession?, call: ApplicationCall) { try { if (!configuration.enableJsonResponse && session == null) { @@ -461,10 +345,9 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } val messages = parseBody(call) ?: return - val initializationRequest = messages.filterIsInstance().firstOrNull { - it.method == Method.Defined.Initialize.value + val isInitializationRequest = messages.any { + it is JSONRPCRequest && it.method == Method.Defined.Initialize.value } - val isInitializationRequest = initializationRequest != null if (isInitializationRequest) { if (initialized.load() && sessionId != null) { @@ -472,17 +355,14 @@ public class StreamableHttpServerTransport(private val configuration: Configurat 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 } @@ -494,84 +374,30 @@ public class StreamableHttpServerTransport(private val configuration: Configurat sessionId?.let { onSessionInitialized?.invoke(it) } } } else { - val requestId = messages.filterIsInstance().firstOrNull()?.id - if (!validateSession(call, requestId) || !validateProtocolVersion(call, requestId)) return + if (!validateSession(call) || !validateProtocolVersion(call)) return } val hasRequest = messages.any { it is JSONRPCRequest } if (!hasRequest) { - call.respond(HttpStatusCode.Accepted) + call.respondNullable(status = HttpStatusCode.Accepted, message = null) 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 + maybeSendPrimingEvent(streamId, session) } 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 + messages.forEach { message -> _onMessage(message) } } catch (e: Exception) { call.reject( HttpStatusCode.BadRequest, @@ -582,11 +408,16 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } } - /** Handles an HTTP GET request by establishing a standalone SSE stream for server-initiated notifications. */ + @Suppress("ReturnCount") 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. + if (configuration.enableJsonResponse) { + call.reject( + HttpStatusCode.MethodNotAllowed, + RPCError.ErrorCode.CONNECTION_CLOSED, + "Method not allowed.", + ) + return + } val sseSession = session ?: error("Server session can't be null for streaming GET requests") val acceptHeader = call.request.header(HttpHeaders.Accept) @@ -608,70 +439,51 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } } - // 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 + if (STANDALONE_SSE_STREAM_ID in streamsMapping) { + call.reject( + HttpStatusCode.Conflict, + RPCError.ErrorCode.CONNECTION_CLOSED, + "Conflict: Only one SSE stream is allowed per session", + ) + return } - 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) + + call.appendSseHeaders() + flushSse(sseSession) // flush headers immediately + streamsMapping[STANDALONE_SSE_STREAM_ID] = SessionContext(sseSession, call) + maybeSendPrimingEvent(STANDALONE_SSE_STREAM_ID, sseSession) + sseSession.coroutineContext.job.invokeOnCompletion { + streamsMapping.remove(STANDALONE_SSE_STREAM_ID) } } - /** 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) + call.respondNullable(status = HttpStatusCode.OK, message = null) } /** * Closes the SSE stream associated with the given [requestId], prompting the client to reconnect. * Useful for implementing polling behavior for long-running operations. */ + @Suppress("ReturnCount", "TooGenericExceptionCaught") 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) - } + try { + sessionContext.session?.close() + } catch (e: Exception) { + _onError(e) + } finally { + streamsMapping.remove(streamId) } } + @Suppress("TooGenericExceptionCaught") private suspend fun replayEvents(store: EventStore, lastEventId: String, session: ServerSSESession) { val call: ApplicationCall = session.call @@ -708,8 +520,8 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } } - // SSE headers are already set by the framework's SSE handler. - flushSse(session) + call.appendSseHeaders() + flushSse(session) // flush headers immediately val streamId = store.replayEventsAfter(lastEventId) { eventId, message -> try { @@ -718,8 +530,6 @@ public class StreamableHttpServerTransport(private val configuration: Configurat id = eventId, data = McpJson.encodeToString(message), ) - } catch (e: CancellationException) { - throw e } catch (e: Exception) { _onError(IllegalStateException("Failed to replay event: ${e.message}", e)) } @@ -731,14 +541,13 @@ public class StreamableHttpServerTransport(private val configuration: Configurat 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 { + @Suppress("ReturnCount") + private suspend fun validateSession(call: ApplicationCall): Boolean { if (sessionIdGenerator == null) return true if (!initialized.load()) { @@ -746,23 +555,32 @@ public class StreamableHttpServerTransport(private val configuration: Configurat HttpStatusCode.BadRequest, RPCError.ErrorCode.CONNECTION_CLOSED, "Bad Request: Server not initialized", - id, ) return false } - val headerId = call.request.header(MCP_SESSION_ID_HEADER) + val sessionHeaderValues = call.request.headers.getAll(MCP_SESSION_ID_HEADER) - if (headerId == null) { + if (sessionHeaderValues.isNullOrEmpty()) { call.reject( HttpStatusCode.BadRequest, RPCError.ErrorCode.CONNECTION_CLOSED, "Bad Request: Mcp-Session-Id header is required", - id, ) return false } + if (sessionHeaderValues.size > 1) { + call.reject( + HttpStatusCode.BadRequest, + RPCError.ErrorCode.CONNECTION_CLOSED, + "Bad Request: Mcp-Session-Id header must be a single value", + ) + return false + } + + val headerId = sessionHeaderValues.single() + return when (headerId) { sessionId -> true @@ -771,15 +589,15 @@ public class StreamableHttpServerTransport(private val configuration: Configurat 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 + private suspend fun validateProtocolVersion(call: ApplicationCall): Boolean { + val protocolVersions = call.request.headers.getAll(MCP_PROTOCOL_VERSION_HEADER) + val version = protocolVersions?.lastOrNull() ?: DEFAULT_NEGOTIATED_PROTOCOL_VERSION return when (version) { !in SUPPORTED_PROTOCOL_VERSIONS -> { @@ -791,7 +609,6 @@ public class StreamableHttpServerTransport(private val configuration: Configurat ", ", ) })", - id, ) false } @@ -800,17 +617,15 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } } + @Suppress("ReturnCount") 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() - } + val hostHeader = call.request.headers[HttpHeaders.Host]?.lowercase() + val allowedHostsLowercase = hosts.map { it.lowercase() } - if (hostname == null || hostname !in allowedHostsLowercase) { + if (hostHeader == null || hostHeader !in allowedHostsLowercase) { return "Invalid Host header: $hostHeader" } } @@ -819,8 +634,7 @@ public class StreamableHttpServerTransport(private val configuration: Configurat 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) { + if (originHeader == null || originHeader !in allowedOriginsLowercase) { return "Invalid Origin header: $originHeader" } } @@ -828,24 +642,33 @@ public class StreamableHttpServerTransport(private val configuration: Configurat return null } + @Suppress("TooGenericExceptionCaught") private suspend fun flushSse(session: ServerSSESession?) { try { session?.send(data = "") - } catch (e: CancellationException) { - throw e } catch (e: Exception) { _onError(e) } } + @Suppress("ReturnCount", "MagicNumber") private suspend fun parseBody(call: ApplicationCall): List? { - val body = try { - call.receiveTextWithLimit(configuration.maxRequestBodySize) - } catch (e: RequestBodyTooLargeException) { + val contentLength = call.request.header(HttpHeaders.ContentLength)?.toIntOrNull() ?: 0 + if (contentLength > MAXIMUM_MESSAGE_SIZE) { call.reject( HttpStatusCode.PayloadTooLarge, RPCError.ErrorCode.INVALID_REQUEST, - "Invalid Request: message size exceeds maximum of ${e.maxBodySize} bytes", + "Invalid Request: message size exceeds maximum of ${MAXIMUM_MESSAGE_SIZE / (1024 * 1024)} MB", + ) + return null + } + + val body = call.receiveText() + if (body.length > MAXIMUM_MESSAGE_SIZE) { + call.reject( + HttpStatusCode.PayloadTooLarge, + RPCError.ErrorCode.INVALID_REQUEST, + "Invalid Request: message size exceeds maximum of ${MAXIMUM_MESSAGE_SIZE / (1024 * 1024)} MB", ) return null } @@ -866,64 +689,29 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } } - /** - * 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 fun String?.accepts(mime: ContentType): Boolean = + this?.lowercase()?.contains(mime.toString().lowercase()) == true 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) - } + streamsMapping.remove(streamId) } } - 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 + @Suppress("TooGenericExceptionCaught") + private suspend fun maybeSendPrimingEvent(streamId: String, session: ServerSSESession?) { + val store = configuration.eventStore ?: return + val sseSession = session ?: return try { val primingEventId = store.storeEvent(streamId, JSONRPCEmptyMessage) - session.send( + sseSession.send( id = primingEventId, retry = configuration.retryInterval?.inWholeMilliseconds, data = "", ) - } catch (e: CancellationException) { - throw e } catch (e: Exception) { _onError(e) } @@ -938,13 +726,7 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } } -/** 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) +internal suspend fun ApplicationCall.reject(status: HttpStatusCode, code: Int, message: String) { + this.response.status(status) + this.respond(JSONRPCError(id = null, error = RPCError(code = code, message = message))) } 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 a05daef..c2cb2c4 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,3 +1,5 @@ +@file:Suppress("TooManyFunctions") + package io.modelcontextprotocol.kotlin.sdk.server import io.github.oshai.kotlinlogging.KotlinLogging 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 deleted file mode 100644 index a63c2e4..0000000 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/utils/ToolNameValidation.kt +++ /dev/null @@ -1,60 +0,0 @@ -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 deleted file mode 100644 index c6bf72b..0000000 --- a/kotlin-sdk-server/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/FeatureRegistryTest.kt +++ /dev/null @@ -1,106 +0,0 @@ -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 deleted file mode 100644 index a172e96..0000000 --- a/kotlin-sdk-server/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSessionAssertCapabilityTest.kt +++ /dev/null @@ -1,147 +0,0 @@ -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 index 7639bc6..28e21b9 100644 --- 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 @@ -14,6 +14,7 @@ import io.ktor.utils.io.readUTF8Line import io.modelcontextprotocol.kotlin.sdk.types.Implementation import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +@Suppress("AbstractClassCanBeConcreteClass") abstract class AbstractKtorExtensionsTest { protected val sseContentType = ContentType("text", "event-stream") 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 deleted file mode 100644 index ea38219..0000000 --- a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/DnsRebindingProtectionTest.kt +++ /dev/null @@ -1,428 +0,0 @@ -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 deleted file mode 100644 index b218b47..0000000 --- a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ElicitationValidationTest.kt +++ /dev/null @@ -1,438 +0,0 @@ -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 index 9888867..05200e5 100644 --- 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 @@ -24,9 +24,9 @@ class KtorApplicationExtensionsTest : AbstractKtorExtensionsTest() { * added to the same application. */ @Test - fun `Application mcp should install SSE and coexist with other routes`() = testApplication { + fun `Application mcp should installs SSE and coexist with other routes`() = testApplication { application { - mcp(enableDnsRebindingProtection = false) { testServer() } + mcp { testServer() } routing { get("/health") { call.respondText("healthy") } 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 index d6b95b4..5ba4448 100644 --- 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 @@ -61,7 +61,7 @@ class KtorRouteExtensionsTest : AbstractKtorExtensionsTest() { route("/api/mcp") { get("/test") { call.respondText("test-endpoint") } - mcp(enableDnsRebindingProtection = false) { testServer() } + mcp { testServer() } } } } @@ -91,7 +91,7 @@ class KtorRouteExtensionsTest : AbstractKtorExtensionsTest() { route("/v1") { route("/services") { route("/mcp") { - mcp(enableDnsRebindingProtection = false) { testServer() } + mcp { testServer() } } } } @@ -112,7 +112,7 @@ class KtorRouteExtensionsTest : AbstractKtorExtensionsTest() { routing { route("/api") { - mcp("/mcp-endpoint", enableDnsRebindingProtection = false) { testServer() } + mcp("/mcp-endpoint") { testServer() } } } } 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 deleted file mode 100644 index dcf3ad2..0000000 --- a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/LogCapture.kt +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index 9dc30cf..0000000 --- a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/RequestBodyTest.kt +++ /dev/null @@ -1,103 +0,0 @@ -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 deleted file mode 100644 index cfcbb62..0000000 --- a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingTest.kt +++ /dev/null @@ -1,104 +0,0 @@ -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 deleted file mode 100644 index a0756f3..0000000 --- a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SseServerTransportTest.kt +++ /dev/null @@ -1,90 +0,0 @@ -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 deleted file mode 100644 index 3646faf..0000000 --- a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StatelessStreamableHttpSessionLifecycleTest.kt +++ /dev/null @@ -1,100 +0,0 @@ -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 c6370a1..dcbb7a4 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 @@ -5,29 +5,17 @@ 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.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.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 @@ -45,12 +33,9 @@ import java.io.ByteArrayOutputStream import java.io.IOException import java.io.PipedInputStream import java.io.PipedOutputStream -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) @@ -86,13 +71,13 @@ class StdioServerTransportTest { @Test fun `should be safe to close before start`() = runIntegrationTest { - val server = StdioServerTransport(input = bufferedInput, output = printOutput) + val server = StdioServerTransport(bufferedInput, printOutput) server.close() // initialized guard makes this a no-op; must not throw } @Test fun `should start then close cleanly`() = runIntegrationTest { - val server = StdioServerTransport(input = bufferedInput, output = printOutput) + val server = StdioServerTransport(bufferedInput, printOutput) server.onError { error -> throw error } @@ -111,7 +96,7 @@ class StdioServerTransportTest { @Test fun `should not read until started`() = runIntegrationTest { - val server = StdioServerTransport(input = bufferedInput, output = printOutput) + val server = StdioServerTransport(bufferedInput, printOutput) server.onError { error -> throw error } @@ -140,7 +125,7 @@ class StdioServerTransportTest { @Test fun `should read multiple messages`() = runIntegrationTest { - val server = StdioServerTransport(input = bufferedInput, output = printOutput) + val server = StdioServerTransport(bufferedInput, printOutput) server.onError { error -> throw error } @@ -177,7 +162,7 @@ class StdioServerTransportTest { @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 server = StdioServerTransport(FaultyRawSource(throwable).buffered(), printOutput) val capturedError = CompletableDeferred() server.onError { capturedError.complete(it) } server.onMessage {} @@ -191,7 +176,7 @@ class StdioServerTransportTest { @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 server = StdioServerTransport(bufferedInput, FaultyRawSink(throwable).buffered()) val capturedError = CompletableDeferred() server.onError { capturedError.complete(it) } server.onMessage {} @@ -205,7 +190,7 @@ class StdioServerTransportTest { @Test fun `should call onClose when input EOF is reached`(): Unit = runIntegrationTest { - val server = StdioServerTransport(input = bufferedInput, output = printOutput) + val server = StdioServerTransport(bufferedInput, printOutput) val didClose = CompletableDeferred() server.onError { throw it } server.onClose { didClose.complete(Unit) } @@ -221,7 +206,7 @@ class StdioServerTransportTest { @Test fun `should throw when starting twice`(): Unit = runIntegrationTest { - val server = StdioServerTransport(input = bufferedInput, output = printOutput) + val server = StdioServerTransport(bufferedInput, printOutput) server.onMessage {} server.start() withClue("Server should not start twice") { @@ -232,24 +217,10 @@ class StdioServerTransportTest { 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 server = StdioServerTransport(bufferedInput, printOutput) val capturedErrors = mutableListOf() val receivedMessages = mutableListOf() val secondMessageProcessed = CompletableDeferred() @@ -282,7 +253,7 @@ class StdioServerTransportTest { @Test fun `should not invoke onError for CancellationException in handler`() = runIntegrationTest { - val server = StdioServerTransport(input = bufferedInput, output = printOutput) + val server = StdioServerTransport(bufferedInput, printOutput) val capturedError = CompletableDeferred() server.onError { capturedError.complete(it) } @@ -310,7 +281,7 @@ class StdioServerTransportTest { @Test fun `should continue receiving valid messages after malformed JSON is skipped`() = runIntegrationTest { - val server = StdioServerTransport(input = bufferedInput, output = printOutput) + val server = StdioServerTransport(bufferedInput, printOutput) val received = CompletableDeferred() // ReadBuffer silently skips unparseable lines — no onError callback expected server.onError {} @@ -328,309 +299,21 @@ class StdioServerTransportTest { 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 - + @Suppress("unused") private fun inputErrors() = listOf( IOException("simulated read failure"), RuntimeException("unexpected read exception"), OutOfMemoryError("unexpected read error"), ) + @Suppress("unused") private fun outputErrors() = listOf( IOException("simulated write failure"), RuntimeException("unexpected write exception"), OutOfMemoryError("unexpected write error"), ) + @Suppress("unused") private fun handlerErrors() = listOf( RuntimeException("handler failure"), IOException("handler IO failure"), 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 deleted file mode 100644 index fc076eb..0000000 --- a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpResponseDeliveryTest.kt +++ /dev/null @@ -1,99 +0,0 @@ -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 deleted file mode 100644 index 1efe2ab..0000000 --- a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerConfigurationTest.kt +++ /dev/null @@ -1,103 +0,0 @@ -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 index 43ff605..c6e539a 100644 --- 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 @@ -2,45 +2,31 @@ 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 @@ -49,37 +35,20 @@ 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 @@ -94,35 +63,6 @@ class StreamableHttpServerTransportTest { 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" @@ -153,90 +93,6 @@ class StreamableHttpServerTransportTest { 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() @@ -272,7 +128,7 @@ class StreamableHttpServerTransportTest { } @Test - fun `second initialization request returns JSON-RPC error with request id`() = testApplication { + fun `second initialization request returns an HTTP error`() = testApplication { configTestServer() val client = createTestClient() @@ -295,74 +151,13 @@ class StreamableHttpServerTransportTest { 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 + secondResponse.status shouldBe HttpStatusCode.BadRequest } @Test @@ -380,15 +175,18 @@ class StreamableHttpServerTransportTest { configureTransportEndpoint(transport) + val initPayload = buildInitializeRequestPayload() val initResponse = client.post(path) { addStreamableHeaders() - setBody(buildInitializeRequestPayload()) + setBody(initPayload) } initResponse.status shouldBe HttpStatusCode.OK val sessionId = initResponse.headers[MCP_SESSION_ID_HEADER] assertNotNull(sessionId) + // TODO When https://github.com/modelcontextprotocol/kotlin-sdk/issues/547 is fixed, + // check the incompatible mcp-protocol-version in the InitializeRequest and delete the part below val response = client.post(path) { addStreamableHeaders() header("mcp-session-id", sessionId) @@ -406,65 +204,6 @@ class StreamableHttpServerTransportTest { } 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 @@ -570,306 +309,6 @@ class StreamableHttpServerTransportTest { 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 { @@ -944,223 +383,6 @@ class StreamableHttpServerTransportTest { 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 { @@ -1215,12 +437,3 @@ class StreamableHttpServerTransportTest { } } } - -/** 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 deleted file mode 100644 index f6760ee..0000000 --- a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/utils/ToolNameValidationTest.kt +++ /dev/null @@ -1,123 +0,0 @@ -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 deleted file mode 100644 index aa33478..0000000 --- a/kotlin-sdk-server/src/jvmTest/resources/logback-test.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - System.err - - [%thread] %level %logger - %msg%n - - - - - - - - - - diff --git a/conformance-test/src/main/resources/simplelogger.properties b/kotlin-sdk-server/src/jvmTest/resources/simplelogger.properties similarity index 82% rename from conformance-test/src/main/resources/simplelogger.properties rename to kotlin-sdk-server/src/jvmTest/resources/simplelogger.properties index 16141d3..95fb646 100644 --- a/conformance-test/src/main/resources/simplelogger.properties +++ b/kotlin-sdk-server/src/jvmTest/resources/simplelogger.properties @@ -6,4 +6,3 @@ 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/kotlin-sdk-testing/build.gradle.kts b/kotlin-sdk-testing/build.gradle.kts index bbb0388..902665c 100644 --- a/kotlin-sdk-testing/build.gradle.kts +++ b/kotlin-sdk-testing/build.gradle.kts @@ -4,7 +4,6 @@ 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) @@ -15,8 +14,10 @@ kotlin { iosArm64() iosX64() iosSimulatorArm64() + watchosX64() watchosArm64() watchosSimulatorArm64() + tvosX64() tvosArm64() tvosSimulatorArm64() js { 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 index ae5f487..e6d19f7 100644 --- 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 @@ -27,8 +27,9 @@ import kotlinx.coroutines.yield * 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. + * @constructor Initializes the transport with the specified [SendChannel] and [ReceiveChannel]. + * @property sendChannel The channel used to send JSON-RPC messages. + * @property receiveChannel The channel used to receive JSON-RPC messages. * @param dispatcher The coroutine dispatcher for the event loop. Defaults to [Dispatchers.Default]. */ @ExperimentalMcpApi @@ -74,19 +75,11 @@ public class ChannelTransport( /** * 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) - * ``` + * This method sets up a client and server transport using Kotlin channels, enabling messages sent + * through one transport to be received by the other. It is typically used for testing or in-memory + * communication without external networking. * * @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( @@ -113,6 +106,7 @@ public class ChannelTransport( override suspend fun initialize() { logger.info { "ChannelTransport starting message processing" } val started = CompletableDeferred() + @Suppress("TooGenericExceptionCaught") scope.launch(CoroutineName("ChannelTransport#${hashCode()}-event-loop")) { try { // Signal that event loop has started, then yield to ensure we're ready 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 index 4aae932..7fc41bb 100644 --- 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 @@ -186,6 +186,7 @@ class ChannelTransportTest { val id = ((msg as JSONRPCRequest).id as RequestId.NumberId).value.toInt() received.add(id) if (id == 2) { + @Suppress("TooGenericExceptionThrown") throw RuntimeException("Error processing message 2") } if (received.size == 4) { 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 13bf272..59faee4 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,3 +1,25 @@ -package io.modelcontextprotocol.kotlin.sdk +@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. + */ -private class Stub +package io.modelcontextprotocol.kotlin.sdk diff --git a/samples/README.md b/samples/README.md deleted file mode 100644 index f831a74..0000000 --- a/samples/README.md +++ /dev/null @@ -1,63 +0,0 @@ -# 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 b10f925..fd9e143 100644 --- a/samples/kotlin-mcp-client/README.md +++ b/samples/kotlin-mcp-client/README.md @@ -1,50 +1,69 @@ # Kotlin MCP Client -An interactive CLI client that connects to any MCP server over STDIO and pipes queries through -Anthropic's Claude API. +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. -## Overview - -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. +For more information about the MCP SDK and protocol, please refer to +the [MCP documentation](https://modelcontextprotocol.io/introduction). ## Prerequisites -- 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`) +- **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 -## Build & Run +## Overview -Run the client, passing the path to an MCP server: +The client application performs the following tasks: + +- **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. + +## Building and Running + +Use the Gradle wrapper to build the application. In a terminal, run: ```shell -# Connect to a JVM server -./gradlew run --args="path/to/server.jar" +./gradlew clean build +``` -# Connect to a Python server -./gradlew run --args="path/to/server.py" +To run the client, execute the jar file and provide the path to your MCP server script. -# Connect to a Node.js server -./gradlew run --args="path/to/build/index.js" +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 ``` > [!NOTE] -> The client uses STDIO transport, so it launches the MCP server as a subprocess. +> The client uses STDIO transport, so it launches the MCP server as a separate process. > Ensure the server script is executable and is a valid `.js`, `.py`, or `.jar` file. -## MCP Capabilities +## Configuration for Anthropic -From the **client** perspective, this sample demonstrates: +Ensure your Anthropic API key is available in your environment: -- **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. +```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. ## Additional Resources -- [MCP Specification](https://modelcontextprotocol.io/specification/latest) +- [MCP Specification](https://spec.modelcontextprotocol.io/) - [Kotlin MCP SDK](https://github.com/modelcontextprotocol/kotlin-sdk) -- [Anthropic Java SDK](https://github.com/anthropics/anthropic-sdk-java) +- [Anthropic Java SDK](https://github.com/anthropics/anthropic-sdk-java/tree/main) diff --git a/samples/kotlin-mcp-client/gradle/libs.versions.toml b/samples/kotlin-mcp-client/gradle/libs.versions.toml index b58eef2..fa8b4e9 100644 --- a/samples/kotlin-mcp-client/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-client/gradle/libs.versions.toml @@ -1,10 +1,10 @@ [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" +anthropic = "2.9.0" +kotlin = "2.2.21" +ktor = "3.2.3" +mcp-kotlin = "0.8.1" +shadow = "9.2.2" +slf4j = "2.0.17" [libraries] anthropic-java = { group = "com.anthropic", name = "anthropic-java", version.ref = "anthropic" } diff --git a/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.jar b/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.jar index eddabd2..61285a6 100644 Binary files a/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.jar 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 index ad7845b..37f78a6 100644 --- a/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.properties +++ b/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.properties @@ -1,9 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.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 index 249efbb..adff685 100755 --- a/samples/kotlin-mcp-client/gradlew +++ b/samples/kotlin-mcp-client/gradlew @@ -20,7 +20,7 @@ ############################################################################## # -# gradlew start up script for POSIX generated by Gradle. +# Gradle 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 gradlew +# ksh Gradle # # 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/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. diff --git a/samples/kotlin-mcp-client/gradlew.bat b/samples/kotlin-mcp-client/gradlew.bat index 8508ef6..c4bdd3a 100644 --- a/samples/kotlin-mcp-client/gradlew.bat +++ b/samples/kotlin-mcp-client/gradlew.bat @@ -1,82 +1,93 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem 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% +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega 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 23fdfcc..8360316 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,11 +2,10 @@ 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 @@ -15,28 +14,24 @@ 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.coroutines.yield import kotlinx.io.asSink import kotlinx.io.asSource import kotlinx.io.buffered import kotlinx.serialization.json.JsonObject -import java.util.concurrent.TimeUnit +import kotlin.jvm.optionals.getOrNull -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() +class MCPClient : AutoCloseable { + // Configures using the `ANTHROPIC_API_KEY` and `ANTHROPIC_AUTH_TOKEN` environment variables + private val anthropic = AnthropicOkHttpClient.fromEnv() // Initialize MCP client private val mcp: Client = Client(clientInfo = Implementation(name = "mcp-client-cli", version = "1.0.0")) - // Server process reference for cleanup - private var serverProcess: Process? = null + private val messageParamsBuilder: MessageCreateParams.Builder = MessageCreateParams.builder() + .model(Model.CLAUDE_4_SONNET_20250514) + .maxTokens(1024) // List of tools offered by the server private lateinit var tools: List @@ -49,52 +44,52 @@ class MCPClient(apiKey: String) : AutoCloseable { // Connect to the server using the path to the server suspend fun connectToServer(serverScriptPath: String) { - // 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") + 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) } - add(serverScriptPath) - } - // Start the server process - val process = withContext(Dispatchers.IO) { - ProcessBuilder(command) - .redirectError(ProcessBuilder.Redirect.INHERIT) - .start() - } - serverProcess = process + // 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() ?: EmptyJsonObject.toJsonValue()) - .putAdditionalProperty("required", JsonValue.from(tool.inputSchema.required)) - .build(), - ) - .build(), + // 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() }}") + } catch (e: Exception) { + println("Failed to connect to MCP server: $e") + throw e } - println("Connected to server with tools: ${tools.joinToString(", ") { it.tool().get().name() }}") } // Process a user query and return a string response @@ -109,29 +104,23 @@ class MCPClient(apiKey: String) : AutoCloseable { // Send the query to the Anthropic model and get the response val response = anthropic.messages().create( - MessageCreateParams.builder() - .model(MODEL) - .maxTokens(MAX_TOKENS) + messageParamsBuilder .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().get().text()) + content.isText() -> finalText.add(content.text().getOrNull()?.text() ?: "") // If the response indicates a tool use, process it further content.isToolUse() -> { - val toolUse = content.toolUse().get() - val toolName = toolUse.name() - val toolUseId = toolUse.id() + val toolName = content.toolUse().get().name() val toolArgs = - toolUse._input().convert(object : TypeReference>() {}) + content.toolUse().get()._input().convert(object : TypeReference>() {}) // Call the tool with provided arguments val result = mcp.callTool( @@ -140,55 +129,37 @@ class MCPClient(apiKey: String) : AutoCloseable { ) finalText.add("[Calling tool $toolName with args $toolArgs]") - // Build tool_result content block with tool_use_id - val toolResultContent = result.content - .filterIsInstance() - .joinToString("\n") { it.text } + // 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(), + ) - val toolResultBlock = ToolResultBlockParam.builder() - .toolUseId(toolUseId) - .content(toolResultContent) - .apply { if (result.isError == true) isError(true) } - .build() + // Retrieve an updated response after tool execution + val aiResponse = anthropic.messages().create( + messageParamsBuilder + .messages(messages) + .build(), + ) - toolResults.add(ContentBlockParam.ofToolResult(toolResultBlock)) + // Append the updated response to final text + finalText.add(aiResponse.content().first().text().getOrNull()?.text() ?: "") } } } - // 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 = "") } @@ -200,27 +171,17 @@ class MCPClient(apiKey: String) : AutoCloseable { while (true) { print("\nQuery: ") val message = readlnOrNull() ?: break - if (message.trim().lowercase() == "quit") break - - try { - val response = processQuery(message) - println("\n$response") - } catch (e: Exception) { - println("\nError: ${e.message}") - } + if (message.lowercase() == "quit") break + val response = processQuery(message) + println("\n$response") + yield() } } 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 001a255..19a3102 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,14 +3,13 @@ package io.modelcontextprotocol.sample.client import kotlinx.coroutines.runBlocking fun main(args: Array) = runBlocking { - 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) + require(args.isNotEmpty()) { + "Usage: java -jar /build/libs/kotlin-mcp-client-0.1.0-all.jar " + } + val serverPath = args.first() + val client = MCPClient() client.use { - client.connectToServer(args.first()) + client.connectToServer(serverPath) client.chatLoop() } } diff --git a/samples/kotlin-mcp-server/README.md b/samples/kotlin-mcp-server/README.md index 82dc5e7..fa56b66 100644 --- a/samples/kotlin-mcp-server/README.md +++ b/samples/kotlin-mcp-server/README.md @@ -1,85 +1,93 @@ -# Kotlin MCP Server +# MCP Kotlin Server Sample -A sample MCP server demonstrating multiple transport modes: STDIO, SSE (plain), and SSE (Ktor -plugin). +A sample implementation of an MCP (Model Context Protocol) server in Kotlin that demonstrates different server +configurations and transport methods. -> **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. +## Features -## Overview +- Multiple server operation modes: + - Standard I/O server + - SSE (Server-Sent Events) server with plain configuration + - SSE server using Ktor plugin +- Built-in capabilities for: + - Prompts management + - Resources handling + - Tools integration -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. +## Getting Started -## Prerequisites +### Running the Server -- JDK 17+ -- [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) (optional, for testing) +The server defaults [STDIO transport](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#stdio). -## Build & Run +You can customize the behavior using command-line arguments. +Logs are printed to [./build/stdout.log](./build/stdout.log) -### STDIO mode (default) +#### Standard I/O mode (STDIO): + +```bash +./gradlew clean build +``` +Use the [MCP inspector](https://modelcontextprotocol.io/docs/tools/inspector) +to connect to MCP via STDIO (Click the "▶️ Connect" button): ```shell -./gradlew run +npx @modelcontextprotocol/inspector --config mcp-inspector-config.json --server stdio-server ``` -Or explicitly: +#### SSE with plain configuration: -```shell -./gradlew run --args="--stdio" -``` +**NB!: 🐞 This configuration may not work ATM** -Connect with the MCP Inspector: - -```shell -npx @modelcontextprotocol/inspector --config samples/kotlin-mcp-server/mcp-inspector-config.json --server stdio-server -``` - -### SSE with Ktor plugin - -```shell -./gradlew run --args="--sse-server-ktor 3002" -``` - -Connect with the MCP Inspector: - -```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 +```bash ./gradlew run --args="--sse-server 3001" ``` - -Connect with the MCP Inspector: - +or ```shell -npx @modelcontextprotocol/inspector --config samples/kotlin-mcp-server/mcp-inspector-config.json --server sse-server +./gradlew clean build +java -jar ./build/libs/kotlin-mcp-server-0.1.0-all.jar --sse-server 3001 ``` -## MCP Capabilities +Use the [MCP inspector](https://modelcontextprotocol.io/docs/tools/inspector) +to connect to `http://localhost:3002/` via SSE Transport (Click the "▶️ Connect" button): +```shell +npx @modelcontextprotocol/inspector --config mcp-inspector-config.json --server sse-server +``` -### Tools +#### SSE with Ktor plugin: -| Name | Description | -|-------------------|------------------------------------------------------| -| `kotlin-sdk-tool` | A test tool that returns a "Hello, world!" greeting. | +```bash +./gradlew run --args="--sse-server-ktor 3002" +``` +or +```shell +./gradlew clean build +java -jar ./build/libs/kotlin-mcp-server-0.1.0-all.jar --sse-server-ktor 3002 +``` -### Prompts +Use the [MCP inspector](https://modelcontextprotocol.io/docs/tools/inspector) +to connect to `http://localhost:3002/` via SSE transport (Click the "▶️ Connect" button): +```shell +npx @modelcontextprotocol/inspector --config mcp-inspector-config.json --server sse-ktor-server +``` -| Name | Description | -|--------------------|------------------------------------------------------------------------------------| -| `Kotlin Developer` | Generates a prompt to develop a small Kotlin application for a given project name. | +## Server Capabilities -### Resources +- **Prompts**: Supports prompt management with list change notifications +- **Resources**: Includes subscription support and list change notifications +- **Tools**: Supports tool management with list change notifications -| Name | URI | Description | -|--------------|-----------------------|---------------------------------------------------------| -| `Web Search` | `https://search.com/` | A placeholder resource demonstrating resource handling. | +## Implementation Details + +The server is implemented using: +- Ktor for HTTP server functionality (SSE modes) +- Kotlin coroutines for asynchronous operations +- SSE for real-time communication in web contexts +- Standard I/O for command-line interface and process-based communication + +## Example Capabilities + +The sample server demonstrates: +- **Prompt**: "Kotlin Developer" - helps develop small Kotlin applications with a configurable project name +- **Tool**: "kotlin-sdk-tool" - a simple test tool that returns a greeting +- **Resource**: "Web Search" - a placeholder resource demonstrating resource handling diff --git a/samples/kotlin-mcp-server/gradle/libs.versions.toml b/samples/kotlin-mcp-server/gradle/libs.versions.toml index d46c2f1..0ce6d58 100644 --- a/samples/kotlin-mcp-server/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-server/gradle/libs.versions.toml @@ -1,9 +1,9 @@ [versions] -kotlin = "2.4.10" -ktor = "3.5.2" -mcp-kotlin = "0.15.0" -slf4j = "2.0.19" -shadow = "9.6.1" +kotlin = "2.2.21" +ktor = "3.2.3" +mcp-kotlin = "0.8.1" +slf4j = "2.0.17" +shadow = "9.2.2" [libraries] ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" } diff --git a/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.jar b/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.jar index eddabd2..61285a6 100644 Binary files a/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.jar 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 index ad7845b..37f78a6 100644 --- a/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.properties +++ b/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.properties @@ -1,9 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.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 index 249efbb..adff685 100755 --- a/samples/kotlin-mcp-server/gradlew +++ b/samples/kotlin-mcp-server/gradlew @@ -20,7 +20,7 @@ ############################################################################## # -# gradlew start up script for POSIX generated by Gradle. +# Gradle 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 gradlew +# ksh Gradle # # 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/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. diff --git a/samples/kotlin-mcp-server/gradlew.bat b/samples/kotlin-mcp-server/gradlew.bat index 8508ef6..c4bdd3a 100644 --- a/samples/kotlin-mcp-server/gradlew.bat +++ b/samples/kotlin-mcp-server/gradlew.bat @@ -1,82 +1,93 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem 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% +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/samples/kotlinlang-mcp-server/Dockerfile b/samples/kotlinlang-mcp-server/Dockerfile deleted file mode 100644 index ada7e56..0000000 --- a/samples/kotlinlang-mcp-server/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index df0dd3e..0000000 --- a/samples/kotlinlang-mcp-server/README.md +++ /dev/null @@ -1,105 +0,0 @@ -# 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 deleted file mode 100644 index f46f006..0000000 --- a/samples/kotlinlang-mcp-server/build.gradle.kts +++ /dev/null @@ -1,42 +0,0 @@ -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 deleted file mode 100644 index 24a5976..0000000 --- a/samples/kotlinlang-mcp-server/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index 0c11d3b..0000000 --- a/samples/kotlinlang-mcp-server/gradle/libs.versions.toml +++ /dev/null @@ -1,31 +0,0 @@ -[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 deleted file mode 100644 index eddabd2..0000000 Binary files a/samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.properties b/samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index ad7845b..0000000 --- a/samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100755 index 249efbb..0000000 --- a/samples/kotlinlang-mcp-server/gradlew +++ /dev/null @@ -1,248 +0,0 @@ -#!/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 deleted file mode 100644 index 8508ef6..0000000 --- a/samples/kotlinlang-mcp-server/gradlew.bat +++ /dev/null @@ -1,82 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem 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 deleted file mode 100644 index bf867e0..0000000 --- a/samples/kotlinlang-mcp-server/settings.gradle.kts +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index e50b1ad..0000000 --- a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/Application.kt +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index 0814342..0000000 --- a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/KotlinlangServer.kt +++ /dev/null @@ -1,118 +0,0 @@ -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 deleted file mode 100644 index 0d0db42..0000000 --- a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/algolia/AlgoliaClient.kt +++ /dev/null @@ -1,38 +0,0 @@ -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 deleted file mode 100644 index 88c465d..0000000 --- a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/algolia/AlgoliaModels.kt +++ /dev/null @@ -1,38 +0,0 @@ -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 deleted file mode 100644 index 7ba8de1..0000000 --- a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/cache/TtlCache.kt +++ /dev/null @@ -1,71 +0,0 @@ -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 deleted file mode 100644 index 0c61e13..0000000 --- a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/config/ServerConfig.kt +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index a8495b2..0000000 --- a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/content/PageFetcher.kt +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index ab0a8ff..0000000 --- a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/content/UrlMapper.kt +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index d4a5097..0000000 --- a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/tools/GetKotlinlangPage.kt +++ /dev/null @@ -1,55 +0,0 @@ -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 deleted file mode 100644 index 886f3f7..0000000 --- a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/tools/SearchKotlinlang.kt +++ /dev/null @@ -1,62 +0,0 @@ -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 deleted file mode 100644 index 4908da5..0000000 --- a/samples/kotlinlang-mcp-server/src/main/resources/application.conf +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 0ffe8a3..0000000 --- a/samples/kotlinlang-mcp-server/src/main/resources/logback.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - %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 deleted file mode 100644 index c6786fb..0000000 --- a/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/KotlinlangServerTest.kt +++ /dev/null @@ -1,62 +0,0 @@ -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 deleted file mode 100644 index 4f9dff6..0000000 --- a/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/algolia/AlgoliaClientTest.kt +++ /dev/null @@ -1,166 +0,0 @@ -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 deleted file mode 100644 index 10dbe5b..0000000 --- a/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/algolia/AlgoliaModelsTest.kt +++ /dev/null @@ -1,126 +0,0 @@ -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 deleted file mode 100644 index a191fe7..0000000 --- a/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/cache/TtlCacheTest.kt +++ /dev/null @@ -1,144 +0,0 @@ -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 deleted file mode 100644 index 5714602..0000000 --- a/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/config/ServerConfigTest.kt +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index 1cd3e6b..0000000 --- a/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/content/PageFetcherTest.kt +++ /dev/null @@ -1,87 +0,0 @@ -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 deleted file mode 100644 index c2076a3..0000000 --- a/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/content/UrlMapperTest.kt +++ /dev/null @@ -1,91 +0,0 @@ -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 deleted file mode 100644 index 18f0cfe..0000000 --- a/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/tools/GetKotlinlangPageTest.kt +++ /dev/null @@ -1,139 +0,0 @@ -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 deleted file mode 100644 index 7e10420..0000000 --- a/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/tools/SearchKotlinlangTest.kt +++ /dev/null @@ -1,231 +0,0 @@ -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 index 54a0953..583039d 100644 --- a/samples/notebooks/McpClient.ipynb +++ b/samples/notebooks/McpClient.ipynb @@ -18,65 +18,52 @@ }, { "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" + "end_time": "2025-12-04T16:08:57.281786Z", + "start_time": "2025-12-04T16:08:57.126434Z" } }, "cell_type": "code", "source": [ "%use coroutines (1.10.2)\n", - "%use ktor-client (0.3.0-98)\n", + "%use ktor-client (0.3.0-5)\n", "\n", "USE {\n", " dependencies(\n", - " \"io.modelcontextprotocol:kotlin-sdk-client-jvm:0.10.0\",\n", + " \"io.modelcontextprotocol:kotlin-sdk-client-jvm:0.8.1\",\n", " )\n", "}" ], "outputs": [], - "execution_count": 8 + "execution_count": 9 }, { "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" + "end_time": "2025-12-04T16:08:57.298752Z", + "start_time": "2025-12-04T16:08:57.285099Z" } }, "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 kotlinx.coroutines.runBlocking\n", + "import io.modelcontextprotocol.kotlin.sdk.EmptyJsonObject\n", "import io.modelcontextprotocol.kotlin.sdk.types.Implementation\n", - "import io.modelcontextprotocol.kotlin.sdk.types.TextContent" + "import io.modelcontextprotocol.kotlin.sdk.types.TextContent\n", + "import io.modelcontextprotocol.kotlin.sdk.client.Client\n", + "import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions\n", + "import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport\n", + "import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport\n", + "import io.ktor.client.HttpClient\n", + "import io.ktor.client.call.body\n", + "import io.ktor.client.request.get\n", + "import kotlinx.serialization.Serializable\n", + "import kotlinx.serialization.json.JsonObject\n", + "import io.ktor.client.plugins.logging.*\n", + "import io.ktor.client.plugins.sse.SSE" ], "outputs": [], - "execution_count": 9 + "execution_count": 10 }, { "metadata": {}, @@ -88,15 +75,9 @@ }, { "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" + "end_time": "2025-12-04T16:08:57.346926Z", + "start_time": "2025-12-04T16:08:57.301809Z" } }, "cell_type": "code", @@ -107,15 +88,14 @@ " 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", + " DISPLAY(HTML(\"
HTTP Log
$message
\"))\n", " }\n", " }\n", " }\n", "}\n" ], "outputs": [], - "execution_count": 10 + "execution_count": 11 }, { "metadata": {}, @@ -124,15 +104,9 @@ }, { "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" + "end_time": "2025-12-04T16:08:57.375128Z", + "start_time": "2025-12-04T16:08:57.350611Z" } }, "cell_type": "code", @@ -141,11 +115,12 @@ " clientInfo = Implementation(\n", " name = \"my-client\",\n", " version = \"1.0.0\"\n", - " )\n", + " ),\n", + " options = ClientOptions(),\n", ")" ], "outputs": [], - "execution_count": 11 + "execution_count": 12 }, { "metadata": {}, @@ -154,15 +129,9 @@ }, { "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" + "end_time": "2025-12-04T16:08:58.199710Z", + "start_time": "2025-12-04T16:08:57.379661Z" } }, "cell_type": "code", @@ -183,7 +152,7 @@ { "data": { "text/html": [ - "
HTTP Request
REQUEST: https://learn.microsoft.com/api/mcp\n",
+       "
HTTP Log
REQUEST: https://learn.microsoft.com/api/mcp\n",
        "METHOD: POST\n",
        "COMMON HEADERS\n",
        "-> Accept: application/json, text/event-stream\n",
@@ -193,7 +162,7 @@
        "-> 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",
+       "{\"id\":\"26884bad1c9b4ea0b9c4470f893b60d3\",\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2025-06-18\",\"capabilities\":{},\"clientInfo\":{\"name\":\"my-client\",\"version\":\"1.0.0\"}},\"jsonrpc\":\"2.0\"}\n",
        "BODY END
" ] }, @@ -206,31 +175,31 @@ { "data": { "text/html": [ - "
HTTP Response
RESPONSE: 200 OK\n",
+       "
HTTP Log
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",
+       "-> Akamai-Cache-Status: Miss from child, Miss from parent\n",
        "-> Cache-Control: no-cache, no-store\n",
        "-> Connection: keep-alive\n",
        "-> Content-Encoding: identity\n",
-       "-> Content-Length: 1983\n",
+       "-> Content-Length: 1947\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",
+       "-> Date: Thu, 04 Dec 2025 16:08:57 GMT\n",
+       "-> Expires: Thu, 04 Dec 2025 16:08:57 GMT\n",
+       "-> Mcp-Session-Id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIn0sInVzZXJJZENsYWltIjpudWxsfQ\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",
+       "-> x-azure-ref: 20251204T160857Z-r1cfc47d796grb7bhC1STOs2rc0000002u20000000002xwb\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",
+       "data: {\"result\":{\"protocolVersion\":\"2025-06-18\",\"capabilities\":{\"logging\":{},\"prompts\":{},\"resources\":{},\"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\":\"26884bad1c9b4ea0b9c4470f893b60d3\",\"jsonrpc\":\"2.0\"}\n",
        "\n",
        "\n",
        "BODY END
" @@ -245,12 +214,12 @@ { "data": { "text/html": [ - "
HTTP Request
REQUEST: https://learn.microsoft.com/api/mcp\n",
+       "
HTTP Log
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",
+       "-> mcp-session-id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIn0sInVzZXJJZENsYWltIjpudWxsfQ\n",
        "CONTENT HEADERS\n",
        "-> Content-Length: 54\n",
        "-> Content-Type: application/json\n",
@@ -269,26 +238,26 @@
     {
      "data": {
       "text/html": [
-       "
HTTP Response
RESPONSE: 202 Accepted\n",
+       "
HTTP Log
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",
+       "-> Akamai-Cache-Status: Miss from child, Miss 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",
+       "-> Date: Thu, 04 Dec 2025 16:08:57 GMT\n",
+       "-> Expires: Thu, 04 Dec 2025 16:08:57 GMT\n",
+       "-> Mcp-Session-Id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIn0sInVzZXJJZENsYWltIjpudWxsfQ\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",
+       "-> x-azure-ref: 20251204T160857Z-r1cfc47d796r8qqwhC1STO91g00000001p6g000000002rdg\n",
        "BODY Content-Type: null\n",
        "BODY START\n",
        "\n",
@@ -301,22 +270,15 @@
       "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",
+       "
HTTP Log
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",
+       "-> mcp-session-id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIn0sInVzZXJJZENsYWltIjpudWxsfQ\n",
        "CONTENT HEADERS\n",
        "-> Content-Length: 0\n",
        "-> Accept: text/event-stream\n",
@@ -337,225 +299,263 @@
      "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" + "✅ Server version: Implementation(name=Microsoft Learn MCP Server, version=1.0.0, title=null, websiteUrl=null, icons=null)\n", + "✅ Server capabilities: ServerCapabilities(tools=Tools(listChanged=true), resources=Resources(listChanged=null, subscribe=null), prompts=Prompts(listChanged=null), logging={}, completions=null, experimental=null)\n" ] } ], "execution_count": 13 }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Now we can send ping request to MCP server:" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-12-04T16:08:58.507109Z", + "start_time": "2025-12-04T16:08:58.206412Z" + } + }, + "cell_type": "code", + "source": [ + "runBlocking {\n", + " mcpClient.ping()\n", + "}" + ], + "outputs": [ + { + "data": { + "text/html": [ + "
HTTP Log
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: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIn0sInVzZXJJZENsYWltIjpudWxsfQ\n",
+       "CONTENT HEADERS\n",
+       "-> Content-Length: 73\n",
+       "-> Content-Type: application/json\n",
+       "BODY Content-Type: application/json\n",
+       "BODY START\n",
+       "{\"id\":\"caf63f9816a44766a926944329e987d5\",\"method\":\"ping\",\"jsonrpc\":\"2.0\"}\n",
+       "BODY END
" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/html": [ + "
HTTP Log
RESPONSE: 200 OK\n",
+       "METHOD: POST\n",
+       "FROM: https://learn.microsoft.com/api/mcp\n",
+       "COMMON HEADERS\n",
+       "-> Akamai-Cache-Status: Miss from child, Miss from parent\n",
+       "-> Cache-Control: no-cache, no-store\n",
+       "-> Connection: keep-alive\n",
+       "-> Content-Encoding: identity\n",
+       "-> Content-Length: 92\n",
+       "-> Content-Type: text/event-stream\n",
+       "-> Date: Thu, 04 Dec 2025 16:08:58 GMT\n",
+       "-> Expires: Thu, 04 Dec 2025 16:08:58 GMT\n",
+       "-> Mcp-Session-Id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIn0sInVzZXJJZENsYWltIjpudWxsfQ\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",
+       "-> 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: 20251204T160858Z-r195dfd6579k9c28hC1STOn4n80000000nk0000000006rvx\n",
+       "BODY Content-Type: text/event-stream\n",
+       "BODY START\n",
+       "event: message\n",
+       "data: {\"result\":{},\"id\":\"caf63f9816a44766a926944329e987d5\",\"jsonrpc\":\"2.0\"}\n",
+       "\n",
+       "\n",
+       "BODY END
" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "EmptyResult(meta=null)" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 14 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Let's get a list of available tools:" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-12-04T16:08:58.818668Z", + "start_time": "2025-12-04T16:08:58.530569Z" + } + }, + "cell_type": "code", + "source": [ + "runBlocking {\n", + " val listToolsResult = mcpClient.listTools()\n", + "\n", + " for (tool in listToolsResult.tools) {\n", + " DISPLAY(tool)\n", + " }\n", + "}\n" + ], + "outputs": [ + { + "data": { + "text/html": [ + "
HTTP Log
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: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIn0sInVzZXJJZENsYWltIjpudWxsfQ\n",
+       "CONTENT HEADERS\n",
+       "-> Content-Length: 79\n",
+       "-> Content-Type: application/json\n",
+       "BODY Content-Type: application/json\n",
+       "BODY START\n",
+       "{\"id\":\"1c46ee9521954378958971e267d543fc\",\"method\":\"tools/list\",\"jsonrpc\":\"2.0\"}\n",
+       "BODY END
" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/html": [ + "
HTTP Log
RESPONSE: 200 OK\n",
+       "METHOD: POST\n",
+       "FROM: https://learn.microsoft.com/api/mcp\n",
+       "COMMON HEADERS\n",
+       "-> Akamai-Cache-Status: Miss from child, Miss from parent\n",
+       "-> Cache-Control: no-cache, no-store\n",
+       "-> Connection: keep-alive\n",
+       "-> Content-Encoding: identity\n",
+       "-> Content-Length: 4495\n",
+       "-> Content-Type: text/event-stream\n",
+       "-> Date: Thu, 04 Dec 2025 16:08:58 GMT\n",
+       "-> Expires: Thu, 04 Dec 2025 16:08:58 GMT\n",
+       "-> Mcp-Session-Id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIn0sInVzZXJJZENsYWltIjpudWxsfQ\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",
+       "-> 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: 20251204T160858Z-r195dfd6579k9c28hC1STOn4n80000000nk0000000006rw8\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\\nThe \\u0060question\\u0060 parameter is no longer used, use \\u0060query\\u0060 instead.\\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},\"question\":{\"description\":\"this parameter is no longer used, use query instead.\",\"type\":\"string\",\"default\":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\"]},\"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 page to markdown format. This tool retrieves the latest complete content of Microsoft documentation pages 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 link from the microsoft.com domain.\\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\":\"1c46ee9521954378958971e267d543fc\",\"jsonrpc\":\"2.0\"}\n",
+       "\n",
+       "\n",
+       "BODY END
" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Tool(name=microsoft_docs_search, inputSchema=ToolSchema(properties={\"query\":{\"description\":\"a query or topic about Microsoft/Azure products, services, platforms, developer tools, frameworks, or APIs\",\"type\":\"string\",\"default\":null},\"question\":{\"description\":\"this parameter is no longer used, use query instead.\",\"type\":\"string\",\"default\":null}}, required=null), 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", + "The `question` parameter is no longer used, use `query` instead.\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., outputSchema=null, title=Microsoft Docs Search, annotations=ToolAnnotations(title=Microsoft Docs Search, readOnlyHint=true, destructiveHint=false, idempotentHint=true, openWorldHint=null), icons=null, meta=null)" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Tool(name=microsoft_code_sample_search, inputSchema=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]), 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, outputSchema=null, title=Microsoft Code Sample Search, annotations=ToolAnnotations(title=Microsoft Code Sample Search, readOnlyHint=true, destructiveHint=false, idempotentHint=true, openWorldHint=null), icons=null, meta=null)" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Tool(name=microsoft_docs_fetch, inputSchema=ToolSchema(properties={\"url\":{\"description\":\"URL of the Microsoft documentation page to read\",\"type\":\"string\"}}, required=[url]), description=Fetch and convert a Microsoft Learn documentation page to markdown format. This tool retrieves the latest complete content of Microsoft documentation pages 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 link from the microsoft.com domain.\n", + "\n", + "## Output Format\n", + "markdown with headings, code blocks, tables, and links preserved., outputSchema=null, title=Microsoft Docs Fetch, annotations=ToolAnnotations(title=Microsoft Docs Fetch, readOnlyHint=true, destructiveHint=false, idempotentHint=true, openWorldHint=null), icons=null, meta=null)" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + } + ], + "execution_count": 15 + }, { "metadata": {}, "cell_type": "markdown", @@ -563,15 +563,9 @@ }, { "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" + "end_time": "2025-12-04T16:08:59.912312Z", + "start_time": "2025-12-04T16:08:58.839853Z" } }, "cell_type": "code", @@ -581,19 +575,9 @@ " 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", + " toolCallResult?.let {\n", + " for (content in it.content) {\n", + " println(content)\n", " }\n", " }\n", "}\n" @@ -602,18 +586,18 @@ { "data": { "text/html": [ - "
HTTP Request
REQUEST: https://learn.microsoft.com/api/mcp\n",
+       "
HTTP Log
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",
+       "-> mcp-session-id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIn0sInVzZXJJZENsYWltIjpudWxsfQ\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",
+       "{\"id\":\"c4d77709f5314c9e9f097f08dd756438\",\"method\":\"tools/call\",\"params\":{\"name\":\"microsoft_docs_search\",\"arguments\":{\"query\":\"Linux\"},\"_meta\":{}},\"jsonrpc\":\"2.0\"}\n",
        "BODY END
" ] }, @@ -626,31 +610,31 @@ { "data": { "text/html": [ - "
HTTP Response
RESPONSE: 200 OK\n",
+       "
HTTP Log
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",
+       "-> Akamai-Cache-Status: Miss from child, Miss from parent\n",
        "-> Cache-Control: no-cache, no-store\n",
        "-> Connection: keep-alive\n",
        "-> Content-Encoding: identity\n",
-       "-> Content-Length: 49801\n",
+       "-> Content-Length: 24086\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",
+       "-> Date: Thu, 04 Dec 2025 16:08:59 GMT\n",
+       "-> Expires: Thu, 04 Dec 2025 16:08:59 GMT\n",
+       "-> Mcp-Session-Id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIn0sInVzZXJJZENsYWltIjpudWxsfQ\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",
+       "-> x-azure-ref: 20251204T160859Z-r195dfd6579k9c28hC1STOn4n80000000nk0000000006rws\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",
+       "data: {\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"[{\\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/en-us/linux/install#step-2---choose-a-linux-distribution\\u0022},{\\u0022title\\u0022:\\u0022New to Linux resources for SQL users\\u0022,\\u0022content\\u0022:\\u0022# New to Linux resources for SQL users\\\\n**Applies to:**[SQL Server](https://learn.microsoft.com/en-us/sql/sql-server/sql-docs-navigation-guide?view=sql-server-ver17#applies-to) - Linux\\\\nThis article is intended to provide a learning path through an introduction of Linux concepts. Use the sections in the article as a guided learning path to familiarize yourself with the Linux environment.\\\\nThis isn\\\\u0027t intended to be a comprehensive list 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. [SQL Server is supported](https://learn.microsoft.com/en-us/sql/linux/sql-server-linux-release-notes-2019?view=sql-server-ver17#supported-platforms) on various distributions such as Red Hat Enterprise Linux (RHEL), SUSE Linux Enterprise Server (SLES), and Ubuntu.\\\\n## Get around directories\\\\nAfter you have chosen 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 will help 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 details installing new software to the Debian and Red Hat Linux operating systems.\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/en-us/sql/linux/new-to-linux-resources?view=sql-server-ver17\\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/en-us/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/en-us/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/en-us/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/en-us/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/en-us/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/en-us/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/en-us/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/en-us/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/en-us/linux/), such as training courses, Linux-versions of popular tools, news, and Open Source events.\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/en-us/linux/install\\u0022},{\\u0022title\\u0022:\\u0022Guide for changing your dev environment from Mac to Windows\\u0022,\\u0022content\\u0022:\\u0022# Guide for changing your dev environment from Mac to Windows\\\\n## Command-line shells and terminals\\\\nYou can now install Windows Subsystem for Linux (WSL) to support running a Linux shell within Windows. This means that you can run **bash**, with whichever specific Linux distribution you choose, integrated right inside Windows. Using WSL provides the kind of environment most familiar to Mac users. For example, you use **ls** to list the files in a current directory, not **dir** as you would with the traditional Windows Cmd Shell. To learn about installing and using WSL, see the [Windows Subsystem for Linux Installation Guide](https://learn.microsoft.com/en-us/windows/wsl/install). Linux distributions that you can install on Windows with WSL include:\\\\n1. [Ubuntu 20.04 LTS](https://www.microsoft.com/store/apps/9n6svws3rx71)\\\\n2. [Kali Linux](https://www.microsoft.com/store/apps/9PKR34TNCV07)\\\\n3. [Debian GNU/Linux](https://www.microsoft.com/store/apps/9MSVKQC78PK6)\\\\n4. [openSUSE Leap 15.1](https://www.microsoft.com/store/apps/9NJFZK00FGKV)\\\\n5. [SUSE Linux Enterprise Server 15 SP1](https://www.microsoft.com/store/apps/9PN498VPMF3Z)\\\\nJust to name a few. Find more in the [WSL install docs](https://learn.microsoft.com/en-us/windows/wsl/install-win10#install-your-linux-distribution-of-choice) and install them directly from the [Microsoft Store](https://aka.ms/wslstore).\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/en-us/windows/dev-environment/mac-to-windows#command-line-shells-and-terminals\\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/en-us/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/en-us/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/en-us/linux/images/linux-distributions-list-screenshot.png)\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/en-us/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/en-us/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/en-us/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/en-us/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/en-us/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/en-us/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/en-us/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#run-commands-as-a-superuser\\u0022}]\"}]},\"id\":\"c4d77709f5314c9e9f097f08dd756438\",\"jsonrpc\":\"2.0\"}\n",
        "\n",
        "\n",
        "BODY END
" @@ -666,50 +650,11 @@ "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" + "TextContent(text=[{\"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/en-us/linux/install#step-2---choose-a-linux-distribution\"},{\"title\":\"New to Linux resources for SQL users\",\"content\":\"# New to Linux resources for SQL users\\n**Applies to:**[SQL Server](https://learn.microsoft.com/en-us/sql/sql-server/sql-docs-navigation-guide?view=sql-server-ver17#applies-to) - Linux\\nThis article is intended to provide a learning path through an introduction of Linux concepts. Use the sections in the article as a guided learning path to familiarize yourself with the Linux environment.\\nThis isn\\u0027t intended to be a comprehensive list 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. [SQL Server is supported](https://learn.microsoft.com/en-us/sql/linux/sql-server-linux-release-notes-2019?view=sql-server-ver17#supported-platforms) on various distributions such as Red Hat Enterprise Linux (RHEL), SUSE Linux Enterprise Server (SLES), and Ubuntu.\\n## Get around directories\\nAfter you have chosen 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 will help 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 details installing new software to the Debian and Red Hat Linux operating systems.\",\"contentUrl\":\"https://learn.microsoft.com/en-us/sql/linux/new-to-linux-resources?view=sql-server-ver17\"},{\"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/en-us/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/en-us/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/en-us/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/en-us/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/en-us/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/id-command.png)\",\"contentUrl\":\"https://learn.microsoft.com/en-us/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/en-us/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/en-us/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/en-us/linux/), such as training courses, Linux-versions of popular tools, news, and Open Source events.\",\"contentUrl\":\"https://learn.microsoft.com/en-us/linux/install\"},{\"title\":\"Guide for changing your dev environment from Mac to Windows\",\"content\":\"# Guide for changing your dev environment from Mac to Windows\\n## Command-line shells and terminals\\nYou can now install Windows Subsystem for Linux (WSL) to support running a Linux shell within Windows. This means that you can run **bash**, with whichever specific Linux distribution you choose, integrated right inside Windows. Using WSL provides the kind of environment most familiar to Mac users. For example, you use **ls** to list the files in a current directory, not **dir** as you would with the traditional Windows Cmd Shell. To learn about installing and using WSL, see the [Windows Subsystem for Linux Installation Guide](https://learn.microsoft.com/en-us/windows/wsl/install). Linux distributions that you can install on Windows with WSL include:\\n1. [Ubuntu 20.04 LTS](https://www.microsoft.com/store/apps/9n6svws3rx71)\\n2. [Kali Linux](https://www.microsoft.com/store/apps/9PKR34TNCV07)\\n3. [Debian GNU/Linux](https://www.microsoft.com/store/apps/9MSVKQC78PK6)\\n4. [openSUSE Leap 15.1](https://www.microsoft.com/store/apps/9NJFZK00FGKV)\\n5. [SUSE Linux Enterprise Server 15 SP1](https://www.microsoft.com/store/apps/9PN498VPMF3Z)\\nJust to name a few. Find more in the [WSL install docs](https://learn.microsoft.com/en-us/windows/wsl/install-win10#install-your-linux-distribution-of-choice) and install them directly from the [Microsoft Store](https://aka.ms/wslstore).\",\"contentUrl\":\"https://learn.microsoft.com/en-us/windows/dev-environment/mac-to-windows#command-line-shells-and-terminals\"},{\"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/en-us/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/en-us/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/en-us/linux/images/linux-distributions-list-screenshot.png)\",\"contentUrl\":\"https://learn.microsoft.com/en-us/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/en-us/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/en-us/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/en-us/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/en-us/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/en-us/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/en-us/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#run-commands-as-a-superuser\"}], annotations=null, meta=null)\n" ] } ], - "execution_count": 14 + "execution_count": 16 } ], "metadata": { diff --git a/samples/notebooks/README.md b/samples/notebooks/README.md deleted file mode 100644 index 8db4f3b..0000000 --- a/samples/notebooks/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# 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 deleted file mode 100644 index 27856d7..0000000 --- a/samples/simple-streamable-server/README.md +++ /dev/null @@ -1,75 +0,0 @@ -# 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 deleted file mode 100644 index 0296429..0000000 --- a/samples/simple-streamable-server/build.gradle.kts +++ /dev/null @@ -1,38 +0,0 @@ -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 deleted file mode 100644 index 32cead6..0000000 --- a/samples/simple-streamable-server/gradle.properties +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index 5f35f74..0000000 --- a/samples/simple-streamable-server/gradle/libs.versions.toml +++ /dev/null @@ -1,26 +0,0 @@ -[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 deleted file mode 100644 index eddabd2..0000000 Binary files a/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.properties b/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index ad7845b..0000000 --- a/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100755 index 249efbb..0000000 --- a/samples/simple-streamable-server/gradlew +++ /dev/null @@ -1,248 +0,0 @@ -#!/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 deleted file mode 100644 index 8508ef6..0000000 --- a/samples/simple-streamable-server/gradlew.bat +++ /dev/null @@ -1,82 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem 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 deleted file mode 100644 index 3c36808..0000000 --- a/samples/simple-streamable-server/settings.gradle.kts +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index 161edc7..0000000 --- a/samples/simple-streamable-server/src/main/kotlin/io/modelcontextprotocol/sample/server/main.kt +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index cdffdae..0000000 --- a/samples/simple-streamable-server/src/main/kotlin/io/modelcontextprotocol/sample/server/server.kt +++ /dev/null @@ -1,297 +0,0 @@ -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 deleted file mode 100644 index c4dae46..0000000 --- a/samples/simple-streamable-server/src/test/kotlin/io/modelcontextprotocol/sample/server/ServerIntegrationTest.kt +++ /dev/null @@ -1,146 +0,0 @@ -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 deleted file mode 100644 index ad2d3de..0000000 --- a/samples/simple-streamable-server/src/test/resources/simplelogger.properties +++ /dev/null @@ -1,6 +0,0 @@ -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 f377d55..0e8efc0 100644 --- a/samples/weather-stdio-server/README.md +++ b/samples/weather-stdio-server/README.md @@ -1,67 +1,140 @@ -# Weather STDIO Server +# Kotlin MCP Weather STDIO Server -A minimal MCP server that exposes weather forecast and alert tools using the National Weather -Service API over STDIO transport. +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. -## 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. +For more information about the MCP SDK and protocol, please refer to +the [MCP documentation](https://modelcontextprotocol.io/introduction). ## Prerequisites -- JDK 17+ -- Internet access (the server calls the weather.gov API at runtime) +- 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) -## Build & Run +## MCP Weather Server -Run the server (it communicates via stdin/stdout): +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: ```shell -./gradlew run +./gradlew clean build ``` -### MCP Inspector - -Build the fat JAR first, then connect with the -[MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector): +To run the server: ```shell -./gradlew build -npx @modelcontextprotocol/inspector -- java -jar samples/weather-stdio-server/build/libs/weather-stdio-server-0.1.0-all.jar +java -jar build/libs/weather-stdio-server-0.1.0-all.jar ``` -### Claude Desktop integration +> [!NOTE] +> The server uses STDIO transport, so it is typically launched in an environment where the client connects via standard +> input/output. -Add the following to your Claude Desktop configuration: +## Tool Implementation -```json -{ - "mcpServers": { - "weather": { - "command": "java", - "args": [ - "-jar", - "/absolute/path/to/samples/weather-stdio-server/build/libs/weather-stdio-server-0.1.0-all.jar" - ] - } - } +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 } ``` -## MCP Capabilities +### 2. Weather Alerts Tool -### Tools +This tool retrieves active weather alerts for a US state. -| 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. | +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: + +```json +{ + "mcpServers": { + "weather": { + "command": "java", + "args": [ + "-jar", + "/absolute/path/to/.jar" + ] + } + } +} +``` + +> [!NOTE] +> Replace `/absolute/path/to/.jar` with the actual absolute path to your built jar file. ## Additional Resources -- [MCP Specification](https://modelcontextprotocol.io/specification/latest) +- [MCP Specification](https://spec.modelcontextprotocol.io/) - [Kotlin MCP SDK](https://github.com/modelcontextprotocol/kotlin-sdk) -- [National Weather Service API](https://www.weather.gov/documentation/services-web-api) +- [Ktor Client Documentation](https://ktor.io/docs/welcome.html) +- [Kotlinx Serialization](https://kotlinlang.org/docs/serialization.html) + diff --git a/samples/weather-stdio-server/build.gradle.kts b/samples/weather-stdio-server/build.gradle.kts index d6f5fc2..0420cc0 100644 --- a/samples/weather-stdio-server/build.gradle.kts +++ b/samples/weather-stdio-server/build.gradle.kts @@ -14,12 +14,14 @@ 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) + runtimeOnly(libs.kotlinx.collections.immutable) testImplementation(kotlin("test")) diff --git a/samples/weather-stdio-server/gradle/libs.versions.toml b/samples/weather-stdio-server/gradle/libs.versions.toml index 9d609a9..6056b94 100644 --- a/samples/weather-stdio-server/gradle/libs.versions.toml +++ b/samples/weather-stdio-server/gradle/libs.versions.toml @@ -1,19 +1,22 @@ [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" +collections-immutable = "0.4.0" +coroutines = "1.10.2" +kotlin = "2.2.21" +ktor = "3.2.3" +logging = "7.0.13" +mcp-kotlin = "0.8.1" +shadow = "9.2.2" +slf4j = "2.0.17" [libraries] kotlin-logging = { group = "io.github.oshai", name = "kotlin-logging-jvm", version.ref = "logging" } +kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable-jvm", version.ref = "collections-immutable" } 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" } +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" } slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } diff --git a/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.jar b/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.jar index eddabd2..61285a6 100644 Binary files a/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.jar 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 index ad7845b..37f78a6 100644 --- a/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.properties +++ b/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.properties @@ -1,9 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.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 index 249efbb..adff685 100755 --- a/samples/weather-stdio-server/gradlew +++ b/samples/weather-stdio-server/gradlew @@ -20,7 +20,7 @@ ############################################################################## # -# gradlew start up script for POSIX generated by Gradle. +# Gradle 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 gradlew +# ksh Gradle # # 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/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. diff --git a/samples/weather-stdio-server/gradlew.bat b/samples/weather-stdio-server/gradlew.bat index 8508ef6..c4bdd3a 100644 --- a/samples/weather-stdio-server/gradlew.bat +++ b/samples/weather-stdio-server/gradlew.bat @@ -1,82 +1,93 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem 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% +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega 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 1dee706..f08c436 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,7 +1,6 @@ package io.modelcontextprotocol.sample.server 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 @@ -16,7 +15,6 @@ 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 @@ -34,59 +32,47 @@ import kotlinx.serialization.json.putJsonObject * 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)), - ), - ) + // Base URL for the Weather API + val baseUrl = "https://api.weather.gov" - 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() + // 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") } - done.join() + contentType(ContentType.Application.Json) + } + // Install content negotiation plugin for JSON serialization/deserialization + install(ContentNegotiation) { + json( + Json { + ignoreUnknownKeys = true + prettyPrint = 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 - }, - ) - } -} + // 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 Server.registerTools(httpClient: HttpClient) { // Register a tool to fetch weather alerts by state - addTool( + server.addTool( name = "get_alerts", - description = "Get weather alerts for a US state. Input is a two-letter US state code (e.g. CA, NY)", + description = """ + Get weather alerts for a US state. Input is Two-letter US state code (e.g. CA, NY) + """.trimIndent(), inputSchema = ToolSchema( properties = buildJsonObject { putJsonObject("state") { @@ -96,62 +82,33 @@ private fun Server.registerTools(httpClient: HttpClient) { }, required = listOf("state"), ), - toolAnnotations = ToolAnnotations(readOnlyHint = true, openWorldHint = true), ) { request -> - val state = request.arguments?.get("state")?.jsonPrimitive?.content - ?: return@addTool CallToolResult( - content = listOf(TextContent("The 'state' parameter is required.")), - ) - - if (state.length != 2) { - return@addTool CallToolResult( - content = listOf(TextContent("Invalid state code: '$state'. Must be a two-letter US state code.")), - isError = true, - ) - } - - val stateCode = state.uppercase() - - 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, - ) - }, + 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 - addTool( + server.addTool( name = "get_forecast", - description = "Get weather forecast for a location. Note: only US locations are supported by the NWS API.", + description = """ + Get weather forecast for a specific latitude/longitude + """.trimIndent(), 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 @@ -161,26 +118,23 @@ private fun Server.registerTools(httpClient: HttpClient) { ) } - 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, - ) - }, - ) + val forecast = httpClient.getForecast(latitude, longitude) + + CallToolResult(content = forecast.map { TextContent(it) }) + } + + // Create a transport using standard IO for server communication + val transport = StdioServerTransport( + System.`in`.asInput(), + System.out.asSink().buffered(), + ) + + runBlocking { + val session = server.createSession(transport) + val done = Job() + session.onClose { + done.complete() + } + done.join() } } 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 000ac12..671186c 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,90 +4,88 @@ import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.request.get import kotlinx.serialization.Serializable -import java.util.Locale +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() + } +} // Extension function to fetch weather alerts for a given state -suspend fun HttpClient.getAlerts(state: String): Result> = runCatching { +suspend fun HttpClient.getAlerts(state: String): List { + // Build the URI using the given state code val uri = "/alerts/active/area/$state" - val alerts = this.get(uri).body() + // Request the alerts data from the API + val alerts = this.get(uri).body() - alerts.features.map { feature -> + // Map each alert feature to a formatted string + return alerts.features.map { feature -> """ Event: ${feature.properties.event} Area: ${feature.properties.areaDesc} Severity: ${feature.properties.severity} - Status: ${feature.properties.status} - Headline: ${feature.properties.headline} + Description: ${feature.properties.description} + Instruction: ${feature.properties.instruction} """.trimIndent() } } -// 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 points response from the API +@Serializable +data class Points( + val properties: Properties +) { + @Serializable + data class Properties(val forecast: String) } +// Data class representing the forecast response from the API @Serializable -data class PointsResponse( - val properties: PointsProperties, -) +data class Forecast( + val properties: Properties +) { + @Serializable + data class Properties(val periods: List) -@Serializable -data class PointsProperties( - val forecast: String? = null, -) + @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 ForecastResponse( - val properties: ForecastProperties, -) +data class Alert( + val features: List +) { + @Serializable + data class Feature( + val properties: Properties + ) -@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, -) + @Serializable + data class Properties( + val event: String, val areaDesc: String, val severity: String, + val description: String, val instruction: String?, + ) +} 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 32fd550..5934ee2 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 @@ -13,8 +13,8 @@ fun main(): Unit = runBlocking { 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) + "${System.getProperty("user.dir")}/build/libs/weather-stdio-server-0.1.0-all.jar", + ).redirectErrorStream(true) .start() val transport = StdioClientTransport( @@ -44,7 +44,7 @@ fun main(): Unit = runBlocking { val alertResult = client.callTool( - name = "get_alerts", + name = "get_alert", arguments = mapOf("state" to "TX"), ).content.map { if (it is TextContent) it.text else it.toString() } diff --git a/test-utils/build.gradle.kts b/test-utils/build.gradle.kts index c6baf2d..91574e0 100644 --- a/test-utils/build.gradle.kts +++ b/test-utils/build.gradle.kts @@ -4,7 +4,6 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl plugins { id("mcp.multiplatform") - id("mcp.detekt") } kotlin { @@ -12,8 +11,10 @@ kotlin { iosArm64() iosX64() iosSimulatorArm64() + watchosX64() watchosArm64() watchosSimulatorArm64() + tvosX64() tvosArm64() tvosSimulatorArm64() js { diff --git a/test-utils/detekt-baseline-main.xml b/test-utils/detekt-baseline-main.xml deleted file mode 100644 index 431f3d9..0000000 --- a/test-utils/detekt-baseline-main.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - 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/detekt-baseline.xml b/test-utils/detekt-baseline.xml new file mode 100644 index 0000000..5abd0b4 --- /dev/null +++ b/test-utils/detekt-baseline.xml @@ -0,0 +1,8 @@ + + + + + CyclomaticComplexMethod:Retry.kt:RetryExtension$private fun executeWithRetry + ThrowsCount:Retry.kt:RetryExtension$private fun executeWithRetry + + diff --git a/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/Retry.kt b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/Retry.kt index 1dcf585..a37e6e3 100644 --- a/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/Retry.kt +++ b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/Retry.kt @@ -16,6 +16,7 @@ import java.util.Optional @ExtendWith(RetryExtension::class) public annotation class Retry(val times: Int = 3, val delayMs: Long = 1000) +@Suppress("ForbiddenVoid") // needed for Java API interop public class RetryExtension : InvocationInterceptor { override fun interceptTestMethod( invocation: Invocation, 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 index ff9292e..e7f5798 100644 --- 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 @@ -18,6 +18,7 @@ public object TypeScriptRunner { * @param logPrefix Prefix for log lines * @return The started Process */ + @Suppress("LongParameterList") public fun run( typescriptDir: File, scriptPath: String, @@ -53,6 +54,7 @@ public object TypeScriptRunner { * @param typescriptDir The directory containing package.json * @throws RuntimeException if npm install fails */ + @Suppress("ReturnCount") public fun installDependencies(typescriptDir: File) { require(typescriptDir.isDirectory()) { "Type script directory does not exist: ${typescriptDir.absolutePath}" } println("Installing TypeScript dependencies in ${typescriptDir.absolutePath}") 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 index a80c825..71fcfa6 100644 --- 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 @@ -14,6 +14,7 @@ import kotlin.time.Duration.Companion.seconds */ public fun killProcessOnPort(port: Int) { val killCommand = if (isWindows) { + @Suppress("MaxLineLength") listOf( "cmd.exe", "/c", 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 index 32f55c9..ee73162 100644 --- 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 @@ -8,6 +8,7 @@ import kotlin.coroutines.CoroutineContext import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds +@Suppress("InjectDispatcher") // this is constant private val defaultTestDispatcher: CoroutineContext = Dispatchers.IO.limitedParallelism(100) private val defaultTestTimeout: Duration = 5.seconds