├── .gitignore ├── .idea ├── .gitignore ├── gradle.xml ├── icon.svg ├── jsLibraryMappings.xml ├── kotlinc.xml ├── misc.xml ├── uiDesigner.xml └── vcs.xml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.gradle.kts ├── docs ├── -m-c-p -kotlin -s-d-k │ ├── org.jetbrains.kotlinx.mcp.client │ │ ├── -client-options │ │ │ ├── -client-options.html │ │ │ ├── capabilities.html │ │ │ └── index.html │ │ ├── -client │ │ │ ├── -client.html │ │ │ ├── assert-capability-for-method.html │ │ │ ├── assert-request-handler-capability.html │ │ │ ├── call-tool.html │ │ │ ├── complete.html │ │ │ ├── connect.html │ │ │ ├── get-prompt.html │ │ │ ├── get-server-capabilities.html │ │ │ ├── get-server-version.html │ │ │ ├── index.html │ │ │ ├── list-prompts.html │ │ │ ├── list-resource-templates.html │ │ │ ├── list-resources.html │ │ │ ├── list-tools.html │ │ │ ├── ping.html │ │ │ ├── read-resource.html │ │ │ ├── send-roots-list-changed.html │ │ │ ├── set-logging-level.html │ │ │ ├── subscribe-resource.html │ │ │ └── unsubscribe-resource.html │ │ ├── -s-s-e-client-transport │ │ │ ├── -s-s-e-client-transport.html │ │ │ ├── close.html │ │ │ ├── index.html │ │ │ ├── on-close.html │ │ │ ├── on-error.html │ │ │ ├── on-message.html │ │ │ ├── send.html │ │ │ └── start.html │ │ ├── -stdio-client-transport │ │ │ ├── -stdio-client-transport.html │ │ │ ├── close.html │ │ │ ├── index.html │ │ │ ├── on-close.html │ │ │ ├── on-error.html │ │ │ ├── on-message.html │ │ │ ├── send.html │ │ │ └── start.html │ │ ├── -web-socket-client-transport │ │ │ ├── -web-socket-client-transport.html │ │ │ ├── index.html │ │ │ └── session.html │ │ ├── index.html │ │ ├── mcp-sse-transport.html │ │ ├── mcp-sse.html │ │ ├── mcp-web-socket-transport.html │ │ └── mcp-web-socket.html │ ├── org.jetbrains.kotlinx.mcp.server │ │ ├── -m-c-p.html │ │ ├── -registered-prompt │ │ │ ├── -registered-prompt.html │ │ │ ├── index.html │ │ │ ├── message-provider.html │ │ │ └── prompt.html │ │ ├── -registered-resource │ │ │ ├── -registered-resource.html │ │ │ ├── index.html │ │ │ ├── read-handler.html │ │ │ └── resource.html │ │ ├── -registered-tool │ │ │ ├── -registered-tool.html │ │ │ ├── handler.html │ │ │ ├── index.html │ │ │ └── tool.html │ │ ├── -s-s-e-server-transport │ │ │ ├── -s-s-e-server-transport.html │ │ │ ├── close.html │ │ │ ├── handle-message.html │ │ │ ├── handle-post-message.html │ │ │ ├── index.html │ │ │ ├── on-close.html │ │ │ ├── on-error.html │ │ │ ├── on-message.html │ │ │ ├── send.html │ │ │ ├── session-id.html │ │ │ └── start.html │ │ ├── -server-options │ │ │ ├── -server-options.html │ │ │ ├── capabilities.html │ │ │ └── index.html │ │ ├── -server │ │ │ ├── -server.html │ │ │ ├── add-prompt.html │ │ │ ├── add-prompts.html │ │ │ ├── add-resource.html │ │ │ ├── add-resources.html │ │ │ ├── add-tool.html │ │ │ ├── add-tools.html │ │ │ ├── assert-capability-for-method.html │ │ │ ├── assert-request-handler-capability.html │ │ │ ├── client-capabilities.html │ │ │ ├── client-version.html │ │ │ ├── create-message.html │ │ │ ├── index.html │ │ │ ├── list-roots.html │ │ │ ├── on-close-callback.html │ │ │ ├── on-initialized.html │ │ │ ├── onclose.html │ │ │ ├── ping.html │ │ │ ├── send-logging-message.html │ │ │ ├── send-prompt-list-changed.html │ │ │ ├── send-resource-list-changed.html │ │ │ ├── send-resource-updated.html │ │ │ └── send-tool-list-changed.html │ │ ├── -stdio-server-transport │ │ │ ├── -stdio-server-transport.html │ │ │ ├── close.html │ │ │ ├── index.html │ │ │ ├── on-close.html │ │ │ ├── on-error.html │ │ │ ├── on-message.html │ │ │ ├── send.html │ │ │ └── start.html │ │ ├── -web-socket-mcp-server-transport │ │ │ ├── -web-socket-mcp-server-transport.html │ │ │ └── index.html │ │ ├── index.html │ │ ├── mcp-web-socket-transport.html │ │ └── mcp-web-socket.html │ ├── org.jetbrains.kotlinx.mcp.shared │ │ ├── -d-e-f-a-u-l-t_-r-e-q-u-e-s-t_-t-i-m-e-o-u-t.html │ │ ├── -progress-callback │ │ │ └── index.html │ │ ├── -protocol-options │ │ │ ├── -protocol-options.html │ │ │ ├── enforce-strict-capabilities.html │ │ │ ├── index.html │ │ │ └── timeout.html │ │ ├── -protocol │ │ │ ├── -protocol.html │ │ │ ├── assert-capability-for-method.html │ │ │ ├── assert-request-handler-capability.html │ │ │ ├── close.html │ │ │ ├── connect.html │ │ │ ├── fallback-notification-handler.html │ │ │ ├── fallback-request-handler.html │ │ │ ├── index.html │ │ │ ├── notification-handlers.html │ │ │ ├── notification.html │ │ │ ├── onclose.html │ │ │ ├── onerror.html │ │ │ ├── remove-notification-handler.html │ │ │ ├── remove-request-handler.html │ │ │ ├── request.html │ │ │ ├── set-notification-handler.html │ │ │ ├── set-request-handler.html │ │ │ └── transport.html │ │ ├── -read-buffer │ │ │ ├── -read-buffer.html │ │ │ ├── append.html │ │ │ ├── clear.html │ │ │ ├── index.html │ │ │ └── read-message.html │ │ ├── -request-handler-extra │ │ │ ├── -request-handler-extra.html │ │ │ └── index.html │ │ ├── -request-options │ │ │ ├── -request-options.html │ │ │ ├── index.html │ │ │ ├── on-progress.html │ │ │ └── timeout.html │ │ ├── -transport │ │ │ ├── close.html │ │ │ ├── index.html │ │ │ ├── on-close.html │ │ │ ├── on-error.html │ │ │ ├── on-message.html │ │ │ ├── send.html │ │ │ └── start.html │ │ ├── -web-socket-mcp-transport │ │ │ ├── -web-socket-mcp-transport.html │ │ │ ├── close.html │ │ │ ├── index.html │ │ │ ├── on-close.html │ │ │ ├── on-error.html │ │ │ ├── on-message.html │ │ │ ├── send.html │ │ │ └── start.html │ │ └── index.html │ ├── org.jetbrains.kotlinx.mcp │ │ ├── -blob-resource-contents │ │ │ ├── -blob-resource-contents.html │ │ │ ├── blob.html │ │ │ ├── index.html │ │ │ ├── mime-type.html │ │ │ └── uri.html │ │ ├── -call-tool-request │ │ │ ├── -call-tool-request.html │ │ │ ├── _meta.html │ │ │ ├── arguments.html │ │ │ ├── index.html │ │ │ ├── method.html │ │ │ └── name.html │ │ ├── -call-tool-result-base │ │ │ ├── content.html │ │ │ ├── index.html │ │ │ └── is-error.html │ │ ├── -call-tool-result │ │ │ ├── -call-tool-result.html │ │ │ ├── _meta.html │ │ │ ├── content.html │ │ │ ├── index.html │ │ │ └── is-error.html │ │ ├── -cancelled-notification │ │ │ ├── -cancelled-notification.html │ │ │ ├── _meta.html │ │ │ ├── index.html │ │ │ ├── method.html │ │ │ ├── reason.html │ │ │ └── request-id.html │ │ ├── -client-capabilities │ │ │ ├── -client-capabilities.html │ │ │ ├── -roots │ │ │ │ ├── -roots.html │ │ │ │ ├── index.html │ │ │ │ └── list-changed.html │ │ │ ├── experimental.html │ │ │ ├── index.html │ │ │ ├── roots.html │ │ │ └── sampling.html │ │ ├── -client-notification │ │ │ └── index.html │ │ ├── -client-request │ │ │ └── index.html │ │ ├── -client-result │ │ │ └── index.html │ │ ├── -compatibility-call-tool-result │ │ │ ├── -compatibility-call-tool-result.html │ │ │ ├── _meta.html │ │ │ ├── content.html │ │ │ ├── index.html │ │ │ ├── is-error.html │ │ │ └── tool-result.html │ │ ├── -complete-request │ │ │ ├── -argument │ │ │ │ ├── -argument.html │ │ │ │ ├── index.html │ │ │ │ ├── name.html │ │ │ │ └── value.html │ │ │ ├── -complete-request.html │ │ │ ├── _meta.html │ │ │ ├── argument.html │ │ │ ├── index.html │ │ │ ├── method.html │ │ │ └── ref.html │ │ ├── -complete-result │ │ │ ├── -complete-result.html │ │ │ ├── -completion │ │ │ │ ├── -completion.html │ │ │ │ ├── has-more.html │ │ │ │ ├── index.html │ │ │ │ ├── total.html │ │ │ │ └── values.html │ │ │ ├── _meta.html │ │ │ ├── completion.html │ │ │ └── index.html │ │ ├── -create-message-request │ │ │ ├── -create-message-request.html │ │ │ ├── -include-context │ │ │ │ ├── all-servers │ │ │ │ │ └── index.html │ │ │ │ ├── entries.html │ │ │ │ ├── index.html │ │ │ │ ├── none │ │ │ │ │ └── index.html │ │ │ │ ├── this-server │ │ │ │ │ └── index.html │ │ │ │ ├── value-of.html │ │ │ │ └── values.html │ │ │ ├── _meta.html │ │ │ ├── include-context.html │ │ │ ├── index.html │ │ │ ├── max-tokens.html │ │ │ ├── messages.html │ │ │ ├── metadata.html │ │ │ ├── method.html │ │ │ ├── model-preferences.html │ │ │ ├── stop-sequences.html │ │ │ ├── system-prompt.html │ │ │ └── temperature.html │ │ ├── -create-message-result │ │ │ ├── -create-message-result.html │ │ │ ├── _meta.html │ │ │ ├── content.html │ │ │ ├── index.html │ │ │ ├── model.html │ │ │ ├── role.html │ │ │ └── stop-reason.html │ │ ├── -cursor │ │ │ └── index.html │ │ ├── -custom-meta │ │ │ ├── -custom-meta.html │ │ │ ├── _meta.html │ │ │ └── index.html │ │ ├── -custom-request │ │ │ ├── -custom-request.html │ │ │ ├── index.html │ │ │ └── method.html │ │ ├── -embedded-resource │ │ │ ├── -companion │ │ │ │ ├── -t-y-p-e.html │ │ │ │ └── index.html │ │ │ ├── -embedded-resource.html │ │ │ ├── index.html │ │ │ ├── resource.html │ │ │ └── type.html │ │ ├── -empty-request-result │ │ │ ├── -empty-request-result.html │ │ │ ├── _meta.html │ │ │ └── index.html │ │ ├── -error-code │ │ │ ├── -defined │ │ │ │ ├── -connection-closed │ │ │ │ │ └── index.html │ │ │ │ ├── -internal-error │ │ │ │ │ └── index.html │ │ │ │ ├── -invalid-params │ │ │ │ │ └── index.html │ │ │ │ ├── -invalid-request │ │ │ │ │ └── index.html │ │ │ │ ├── -method-not-found │ │ │ │ │ └── index.html │ │ │ │ ├── -parse-error │ │ │ │ │ └── index.html │ │ │ │ ├── -request-timeout │ │ │ │ │ └── index.html │ │ │ │ ├── code.html │ │ │ │ ├── entries.html │ │ │ │ ├── index.html │ │ │ │ ├── value-of.html │ │ │ │ └── values.html │ │ │ ├── -unknown │ │ │ │ ├── -unknown.html │ │ │ │ ├── code.html │ │ │ │ └── index.html │ │ │ ├── code.html │ │ │ └── index.html │ │ ├── -get-prompt-request │ │ │ ├── -get-prompt-request.html │ │ │ ├── _meta.html │ │ │ ├── arguments.html │ │ │ ├── index.html │ │ │ ├── method.html │ │ │ └── name.html │ │ ├── -get-prompt-result │ │ │ ├── -get-prompt-result.html │ │ │ ├── _meta.html │ │ │ ├── description.html │ │ │ ├── index.html │ │ │ └── messages.html │ │ ├── -image-content │ │ │ ├── -companion │ │ │ │ ├── -t-y-p-e.html │ │ │ │ └── index.html │ │ │ ├── -image-content.html │ │ │ ├── data.html │ │ │ ├── index.html │ │ │ ├── mime-type.html │ │ │ └── type.html │ │ ├── -implementation │ │ │ ├── -implementation.html │ │ │ ├── index.html │ │ │ ├── name.html │ │ │ └── version.html │ │ ├── -initialize-request │ │ │ ├── -initialize-request.html │ │ │ ├── _meta.html │ │ │ ├── capabilities.html │ │ │ ├── client-info.html │ │ │ ├── index.html │ │ │ ├── method.html │ │ │ └── protocol-version.html │ │ ├── -initialize-result │ │ │ ├── -initialize-result.html │ │ │ ├── _meta.html │ │ │ ├── capabilities.html │ │ │ ├── index.html │ │ │ ├── protocol-version.html │ │ │ └── server-info.html │ │ ├── -initialized-notification │ │ │ ├── -initialized-notification.html │ │ │ ├── index.html │ │ │ └── method.html │ │ ├── -j-s-o-n-r-p-c-error │ │ │ ├── -j-s-o-n-r-p-c-error.html │ │ │ ├── code.html │ │ │ ├── data.html │ │ │ ├── index.html │ │ │ └── message.html │ │ ├── -j-s-o-n-r-p-c-message │ │ │ └── index.html │ │ ├── -j-s-o-n-r-p-c-notification │ │ │ ├── -j-s-o-n-r-p-c-notification.html │ │ │ ├── index.html │ │ │ ├── jsonrpc.html │ │ │ ├── method.html │ │ │ └── params.html │ │ ├── -j-s-o-n-r-p-c-request │ │ │ ├── -j-s-o-n-r-p-c-request.html │ │ │ ├── id.html │ │ │ ├── index.html │ │ │ ├── jsonrpc.html │ │ │ ├── method.html │ │ │ └── params.html │ │ ├── -j-s-o-n-r-p-c-response │ │ │ ├── -j-s-o-n-r-p-c-response.html │ │ │ ├── error.html │ │ │ ├── id.html │ │ │ ├── index.html │ │ │ ├── jsonrpc.html │ │ │ └── result.html │ │ ├── -j-s-o-n-r-p-c_-v-e-r-s-i-o-n.html │ │ ├── -l-a-t-e-s-t_-p-r-o-t-o-c-o-l_-v-e-r-s-i-o-n.html │ │ ├── -list-prompts-request │ │ │ ├── -list-prompts-request.html │ │ │ ├── _meta.html │ │ │ ├── cursor.html │ │ │ ├── index.html │ │ │ └── method.html │ │ ├── -list-prompts-result │ │ │ ├── -list-prompts-result.html │ │ │ ├── _meta.html │ │ │ ├── index.html │ │ │ ├── next-cursor.html │ │ │ └── prompts.html │ │ ├── -list-resource-templates-request │ │ │ ├── -list-resource-templates-request.html │ │ │ ├── _meta.html │ │ │ ├── cursor.html │ │ │ ├── index.html │ │ │ └── method.html │ │ ├── -list-resource-templates-result │ │ │ ├── -list-resource-templates-result.html │ │ │ ├── _meta.html │ │ │ ├── index.html │ │ │ ├── next-cursor.html │ │ │ └── resource-templates.html │ │ ├── -list-resources-request │ │ │ ├── -list-resources-request.html │ │ │ ├── _meta.html │ │ │ ├── cursor.html │ │ │ ├── index.html │ │ │ └── method.html │ │ ├── -list-resources-result │ │ │ ├── -list-resources-result.html │ │ │ ├── _meta.html │ │ │ ├── index.html │ │ │ ├── next-cursor.html │ │ │ └── resources.html │ │ ├── -list-roots-request │ │ │ ├── -list-roots-request.html │ │ │ ├── _meta.html │ │ │ ├── index.html │ │ │ └── method.html │ │ ├── -list-roots-result │ │ │ ├── -list-roots-result.html │ │ │ ├── _meta.html │ │ │ ├── index.html │ │ │ └── roots.html │ │ ├── -list-tools-request │ │ │ ├── -list-tools-request.html │ │ │ ├── _meta.html │ │ │ ├── cursor.html │ │ │ ├── index.html │ │ │ └── method.html │ │ ├── -list-tools-result │ │ │ ├── -list-tools-result.html │ │ │ ├── _meta.html │ │ │ ├── index.html │ │ │ ├── next-cursor.html │ │ │ └── tools.html │ │ ├── -logging-level │ │ │ ├── alert │ │ │ │ └── index.html │ │ │ ├── critical │ │ │ │ └── index.html │ │ │ ├── debug │ │ │ │ └── index.html │ │ │ ├── emergency │ │ │ │ └── index.html │ │ │ ├── entries.html │ │ │ ├── error │ │ │ │ └── index.html │ │ │ ├── index.html │ │ │ ├── info │ │ │ │ └── index.html │ │ │ ├── notice │ │ │ │ └── index.html │ │ │ ├── value-of.html │ │ │ ├── values.html │ │ │ └── warning │ │ │ │ └── index.html │ │ ├── -logging-message-notification │ │ │ ├── -logging-message-notification.html │ │ │ ├── -set-level-request │ │ │ │ ├── -set-level-request.html │ │ │ │ ├── _meta.html │ │ │ │ ├── index.html │ │ │ │ ├── level.html │ │ │ │ └── method.html │ │ │ ├── _meta.html │ │ │ ├── data.html │ │ │ ├── index.html │ │ │ ├── level.html │ │ │ ├── logger.html │ │ │ └── method.html │ │ ├── -mcp-error │ │ │ ├── -mcp-error.html │ │ │ ├── code.html │ │ │ ├── data.html │ │ │ ├── index.html │ │ │ └── message.html │ │ ├── -method │ │ │ ├── -custom │ │ │ │ ├── -custom.html │ │ │ │ ├── index.html │ │ │ │ └── value.html │ │ │ ├── -defined │ │ │ │ ├── -completion-complete │ │ │ │ │ └── index.html │ │ │ │ ├── -initialize │ │ │ │ │ └── index.html │ │ │ │ ├── -logging-set-level │ │ │ │ │ └── index.html │ │ │ │ ├── -notifications-cancelled │ │ │ │ │ └── index.html │ │ │ │ ├── -notifications-initialized │ │ │ │ │ └── index.html │ │ │ │ ├── -notifications-message │ │ │ │ │ └── index.html │ │ │ │ ├── -notifications-progress │ │ │ │ │ └── index.html │ │ │ │ ├── -notifications-prompts-list-changed │ │ │ │ │ └── index.html │ │ │ │ ├── -notifications-resources-list-changed │ │ │ │ │ └── index.html │ │ │ │ ├── -notifications-resources-updated │ │ │ │ │ └── index.html │ │ │ │ ├── -notifications-roots-list-changed │ │ │ │ │ └── index.html │ │ │ │ ├── -notifications-tools-list-changed │ │ │ │ │ └── index.html │ │ │ │ ├── -ping │ │ │ │ │ └── index.html │ │ │ │ ├── -prompts-get │ │ │ │ │ └── index.html │ │ │ │ ├── -prompts-list │ │ │ │ │ └── index.html │ │ │ │ ├── -resources-list │ │ │ │ │ └── index.html │ │ │ │ ├── -resources-read │ │ │ │ │ └── index.html │ │ │ │ ├── -resources-subscribe │ │ │ │ │ └── index.html │ │ │ │ ├── -resources-templates-list │ │ │ │ │ └── index.html │ │ │ │ ├── -resources-unsubscribe │ │ │ │ │ └── index.html │ │ │ │ ├── -roots-list │ │ │ │ │ └── index.html │ │ │ │ ├── -sampling-create-message │ │ │ │ │ └── index.html │ │ │ │ ├── -tools-call │ │ │ │ │ └── index.html │ │ │ │ ├── -tools-list │ │ │ │ │ └── index.html │ │ │ │ ├── entries.html │ │ │ │ ├── index.html │ │ │ │ ├── value-of.html │ │ │ │ ├── value.html │ │ │ │ └── values.html │ │ │ ├── index.html │ │ │ └── value.html │ │ ├── -model-hint │ │ │ ├── -model-hint.html │ │ │ ├── index.html │ │ │ └── name.html │ │ ├── -model-preferences │ │ │ ├── -model-preferences.html │ │ │ ├── cost-priority.html │ │ │ ├── hints.html │ │ │ ├── index.html │ │ │ ├── intelligence-priority.html │ │ │ └── speed-priority.html │ │ ├── -notification │ │ │ ├── index.html │ │ │ └── method.html │ │ ├── -paginated-request │ │ │ ├── _meta.html │ │ │ ├── cursor.html │ │ │ └── index.html │ │ ├── -paginated-result │ │ │ ├── index.html │ │ │ └── next-cursor.html │ │ ├── -ping-request │ │ │ ├── -ping-request.html │ │ │ ├── index.html │ │ │ └── method.html │ │ ├── -progress-base │ │ │ ├── index.html │ │ │ ├── progress.html │ │ │ └── total.html │ │ ├── -progress-notification │ │ │ ├── -progress-notification.html │ │ │ ├── _meta.html │ │ │ ├── index.html │ │ │ ├── method.html │ │ │ ├── progress-token.html │ │ │ ├── progress.html │ │ │ └── total.html │ │ ├── -progress-token │ │ │ └── index.html │ │ ├── -progress │ │ │ ├── -progress.html │ │ │ ├── index.html │ │ │ ├── progress.html │ │ │ └── total.html │ │ ├── -prompt-argument │ │ │ ├── -prompt-argument.html │ │ │ ├── description.html │ │ │ ├── index.html │ │ │ ├── name.html │ │ │ └── required.html │ │ ├── -prompt-list-changed-notification │ │ │ ├── -prompt-list-changed-notification.html │ │ │ ├── index.html │ │ │ └── method.html │ │ ├── -prompt-message-content-text-or-image │ │ │ └── index.html │ │ ├── -prompt-message-content │ │ │ ├── index.html │ │ │ └── type.html │ │ ├── -prompt-message │ │ │ ├── -prompt-message.html │ │ │ ├── content.html │ │ │ ├── index.html │ │ │ └── role.html │ │ ├── -prompt-reference │ │ │ ├── -companion │ │ │ │ ├── -t-y-p-e.html │ │ │ │ └── index.html │ │ │ ├── -prompt-reference.html │ │ │ ├── index.html │ │ │ ├── name.html │ │ │ └── type.html │ │ ├── -prompt │ │ │ ├── -prompt.html │ │ │ ├── arguments.html │ │ │ ├── description.html │ │ │ ├── index.html │ │ │ └── name.html │ │ ├── -read-resource-request │ │ │ ├── -read-resource-request.html │ │ │ ├── _meta.html │ │ │ ├── index.html │ │ │ ├── method.html │ │ │ └── uri.html │ │ ├── -read-resource-result │ │ │ ├── -read-resource-result.html │ │ │ ├── _meta.html │ │ │ ├── contents.html │ │ │ └── index.html │ │ ├── -reference │ │ │ ├── index.html │ │ │ └── type.html │ │ ├── -request-id │ │ │ └── index.html │ │ ├── -request-result │ │ │ └── index.html │ │ ├── -request │ │ │ ├── index.html │ │ │ └── method.html │ │ ├── -resource-contents │ │ │ ├── index.html │ │ │ ├── mime-type.html │ │ │ └── uri.html │ │ ├── -resource-list-changed-notification │ │ │ ├── -resource-list-changed-notification.html │ │ │ ├── index.html │ │ │ └── method.html │ │ ├── -resource-reference │ │ │ ├── -companion │ │ │ │ ├── -t-y-p-e.html │ │ │ │ └── index.html │ │ │ ├── -resource-reference.html │ │ │ ├── index.html │ │ │ ├── type.html │ │ │ └── uri.html │ │ ├── -resource-template │ │ │ ├── -resource-template.html │ │ │ ├── description.html │ │ │ ├── index.html │ │ │ ├── mime-type.html │ │ │ ├── name.html │ │ │ └── uri-template.html │ │ ├── -resource-updated-notification │ │ │ ├── -resource-updated-notification.html │ │ │ ├── _meta.html │ │ │ ├── index.html │ │ │ ├── method.html │ │ │ └── uri.html │ │ ├── -resource │ │ │ ├── -resource.html │ │ │ ├── description.html │ │ │ ├── index.html │ │ │ ├── mime-type.html │ │ │ ├── name.html │ │ │ └── uri.html │ │ ├── -role │ │ │ ├── assistant │ │ │ │ └── index.html │ │ │ ├── entries.html │ │ │ ├── index.html │ │ │ ├── user │ │ │ │ └── index.html │ │ │ ├── value-of.html │ │ │ └── values.html │ │ ├── -root │ │ │ ├── -root.html │ │ │ ├── index.html │ │ │ ├── name.html │ │ │ └── uri.html │ │ ├── -roots-list-changed-notification │ │ │ ├── -roots-list-changed-notification.html │ │ │ ├── index.html │ │ │ └── method.html │ │ ├── -s-u-p-p-o-r-t-e-d_-p-r-o-t-o-c-o-l_-v-e-r-s-i-o-n-s.html │ │ ├── -sampling-message │ │ │ ├── -sampling-message.html │ │ │ ├── content.html │ │ │ ├── index.html │ │ │ └── role.html │ │ ├── -server-capabilities │ │ │ ├── -prompts │ │ │ │ ├── -prompts.html │ │ │ │ ├── index.html │ │ │ │ └── list-changed.html │ │ │ ├── -resources │ │ │ │ ├── -resources.html │ │ │ │ ├── index.html │ │ │ │ ├── list-changed.html │ │ │ │ └── subscribe.html │ │ │ ├── -server-capabilities.html │ │ │ ├── -tools │ │ │ │ ├── -tools.html │ │ │ │ ├── index.html │ │ │ │ └── list-changed.html │ │ │ ├── experimental.html │ │ │ ├── index.html │ │ │ ├── logging.html │ │ │ ├── prompts.html │ │ │ ├── resources.html │ │ │ ├── sampling.html │ │ │ └── tools.html │ │ ├── -server-notification │ │ │ └── index.html │ │ ├── -server-request │ │ │ └── index.html │ │ ├── -server-result │ │ │ └── index.html │ │ ├── -stop-reason │ │ │ ├── -end-turn │ │ │ │ ├── index.html │ │ │ │ └── value.html │ │ │ ├── -max-tokens │ │ │ │ ├── index.html │ │ │ │ └── value.html │ │ │ ├── -other │ │ │ │ ├── -other.html │ │ │ │ ├── index.html │ │ │ │ └── value.html │ │ │ ├── -stop-sequence │ │ │ │ ├── index.html │ │ │ │ └── value.html │ │ │ ├── index.html │ │ │ └── value.html │ │ ├── -subscribe-request │ │ │ ├── -subscribe-request.html │ │ │ ├── _meta.html │ │ │ ├── index.html │ │ │ ├── method.html │ │ │ └── uri.html │ │ ├── -text-content │ │ │ ├── -companion │ │ │ │ ├── -t-y-p-e.html │ │ │ │ └── index.html │ │ │ ├── -text-content.html │ │ │ ├── index.html │ │ │ ├── text.html │ │ │ └── type.html │ │ ├── -text-resource-contents │ │ │ ├── -text-resource-contents.html │ │ │ ├── index.html │ │ │ ├── mime-type.html │ │ │ ├── text.html │ │ │ └── uri.html │ │ ├── -tool-list-changed-notification │ │ │ ├── -tool-list-changed-notification.html │ │ │ ├── index.html │ │ │ └── method.html │ │ ├── -tool │ │ │ ├── -input │ │ │ │ ├── -input.html │ │ │ │ ├── index.html │ │ │ │ ├── properties.html │ │ │ │ └── type.html │ │ │ ├── -tool.html │ │ │ ├── description.html │ │ │ ├── index.html │ │ │ ├── input-schema.html │ │ │ └── name.html │ │ ├── -unknown-content │ │ │ ├── -unknown-content.html │ │ │ ├── index.html │ │ │ └── type.html │ │ ├── -unknown-method-request-or-notification │ │ │ ├── -unknown-method-request-or-notification.html │ │ │ ├── index.html │ │ │ └── method.html │ │ ├── -unknown-reference │ │ │ ├── -unknown-reference.html │ │ │ ├── index.html │ │ │ └── type.html │ │ ├── -unknown-resource-contents │ │ │ ├── -unknown-resource-contents.html │ │ │ ├── index.html │ │ │ ├── mime-type.html │ │ │ └── uri.html │ │ ├── -unsubscribe-request │ │ │ ├── -unsubscribe-request.html │ │ │ ├── _meta.html │ │ │ ├── index.html │ │ │ ├── method.html │ │ │ └── uri.html │ │ ├── -with-meta │ │ │ ├── -companion │ │ │ │ ├── -empty.html │ │ │ │ └── index.html │ │ │ ├── _meta.html │ │ │ └── index.html │ │ ├── from-j-s-o-n.html │ │ ├── index.html │ │ └── to-j-s-o-n.html │ ├── package-list │ └── shared │ │ ├── -l-i-b_-v-e-r-s-i-o-n.html │ │ └── index.html ├── images │ ├── anchor-copy-button.svg │ ├── arrow_down.svg │ ├── burger.svg │ ├── copy-icon.svg │ ├── copy-successful-icon.svg │ ├── footer-go-to-link.svg │ ├── go-to-top-icon.svg │ ├── homepage.svg │ ├── logo-icon.svg │ ├── nav-icons │ │ ├── abstract-class-kotlin.svg │ │ ├── abstract-class.svg │ │ ├── annotation-kotlin.svg │ │ ├── annotation.svg │ │ ├── class-kotlin.svg │ │ ├── class.svg │ │ ├── enum-kotlin.svg │ │ ├── enum.svg │ │ ├── exception-class.svg │ │ ├── field-value.svg │ │ ├── field-variable.svg │ │ ├── function.svg │ │ ├── interface-kotlin.svg │ │ ├── interface.svg │ │ ├── object.svg │ │ └── typealias-kotlin.svg │ └── theme-toggle.svg ├── index.html ├── navigation.html ├── scripts │ ├── clipboard.js │ ├── main.js │ ├── navigation-loader.js │ ├── pages.json │ ├── platform-content-handler.js │ ├── prism.js │ ├── sourceset_dependencies.js │ └── symbol-parameters-wrapper_deferred.js ├── styles │ ├── font-jb-sans-auto.css │ ├── logo-styles.css │ ├── main.css │ ├── prism.css │ └── style.css └── ui-kit │ ├── ui-kit.min.css │ └── ui-kit.min.js ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── samples ├── cli.kt ├── connect-to-intellij.sh ├── inspector.sh ├── kotlin-mcp-server │ ├── .gitignore │ ├── .idea │ │ ├── .gitignore │ │ ├── gradle.xml │ │ ├── kotlinc.xml │ │ ├── misc.xml │ │ └── vcs.xml │ ├── README.md │ ├── build.gradle.kts │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle.kts │ └── src │ │ └── main │ │ └── kotlin │ │ └── Main.kt └── node-server │ ├── build.sh │ ├── package-lock.json │ ├── package.json │ ├── run_client.sh │ ├── run_server.sh │ ├── src │ ├── client.ts │ ├── server.ts │ └── tmp.ts │ └── tsconfig.json ├── settings.gradle.kts └── src ├── main └── kotlin │ └── org │ └── jetbrains │ └── kotlinx │ └── mcp │ ├── client │ ├── Client.kt │ ├── SSEClientTransport.kt │ ├── StdioClientTransport.kt │ ├── WebSocketClientTransport.kt │ ├── WebSocketMcpKtorClientExtensions.kt │ └── sse.ktor.kt │ ├── server │ ├── McpKtorServerPlugin.kt │ ├── SSEServerTransport.kt │ ├── Server.kt │ ├── StdioServerTransport.kt │ ├── WebSocketMcpKtorServerExtensions.kt │ └── WebSocketMcpServerTransport.kt │ ├── shared │ ├── Protocol.kt │ ├── ReadBuffer.kt │ ├── Transport.kt │ └── WebSocketMcpTransport.kt │ ├── types.kt │ └── types.util.kt └── test └── kotlin ├── InMemoryTransport.kt ├── client ├── BaseTransportTest.kt ├── ClientIntegrationTest.kt ├── ClientTest.kt ├── InMemoryTransportTest.kt ├── SseTransportTest.kt ├── StdioClientTransportTest.kt ├── TypesTest.kt └── WebSocketTransportTest.kt ├── server └── StdioServerTransportTest.kt ├── shared └── ReadBufferTest.kt └── sse.ktor.kt /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/modules.xml 9 | .idea/jarRepositories.xml 10 | .idea/compiler.xml 11 | .idea/libraries/ 12 | *.iws 13 | *.iml 14 | *.ipr 15 | out/ 16 | !**/src/main/**/out/ 17 | !**/src/test/**/out/ 18 | 19 | ### Kotlin ### 20 | .kotlin 21 | 22 | ### Eclipse ### 23 | .apt_generated 24 | .classpath 25 | .factorypath 26 | .project 27 | .settings 28 | .springBeans 29 | .sts4-cache 30 | bin/ 31 | !**/src/main/**/bin/ 32 | !**/src/test/**/bin/ 33 | 34 | ### NetBeans ### 35 | /nbproject/private/ 36 | /nbbuild/ 37 | /dist/ 38 | /nbdist/ 39 | /.nb-gradle/ 40 | 41 | ### VS Code ### 42 | .vscode/ 43 | 44 | ### Mac OS ### 45 | .DS_Store 46 | 47 | ### Node.js ### 48 | node_modules 49 | dist 50 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 9 | 11 | 19 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | There are two main ways to contribute to the project — submitting issues and submitting 4 | fixes/changes/improvements via pull requests. 5 | 6 | ## Submitting issues 7 | 8 | Both bug reports and feature requests are welcome. 9 | Submit issues [here](https://github.com/JetBrains/mcp-kotlin-sdk/issues). 10 | 11 | * Search for existing issues to avoid reporting duplicates. 12 | * When submitting a bug report: 13 | * Test it against the most recently released version. It might have already been fixed. 14 | * Include the code that reproduces the problem. Provide a minimal, complete, and reproducible example. 15 | * However, don't put off reporting any weird or rarely appearing issues just because you cannot consistently 16 | reproduce them. 17 | * If the bug is related to behavior, explain what behavior you expected and what you got. 18 | * When submitting a feature request: 19 | * Explain why you need the feature — what's your use-case, what's your domain. 20 | * Explaining the problem you face is more important than suggesting a solution. 21 | Even if you don't have a proposed solution, please report your problem. 22 | * If there is an alternative way to do what you need, then show the code of the alternative. 23 | 24 | ## Submitting PRs 25 | 26 | We love PRs. Submit PRs [here](https://github.com/JetBrains/mcp-kotlin-sdk/pulls). 27 | However, please keep in mind that maintainers will have to support the resulting code of the project, 28 | so do familiarize yourself with the following guidelines. 29 | 30 | * All development (both new features and bug fixes) is performed in the `main` branch. 31 | * Please base your PRs on the `main` branch. 32 | * PR should be linked with the issue, excluding minor documentation changes, adding unit tests, and fixing typos. 33 | * If you make any code changes: 34 | * Follow the [Kotlin Coding Conventions](https://kotlinlang.org/docs/reference/coding-conventions.html). 35 | * [Build the project](#building) to ensure it all works and passes the tests. 36 | * If you fix a bug: 37 | * Write the test that reproduces the bug. 38 | * Fixes without tests are accepted only in exceptional circumstances if it can be shown that writing the 39 | corresponding test is too hard or otherwise impractical. 40 | * If you introduce any new public APIs: 41 | * All new APIs must come with documentation and tests. 42 | * If you plan API additions, please start by submitting an issue with the proposed API design to gather community 43 | feedback. 44 | * [Contact the maintainers](#contacting-maintainers) to coordinate any great work in advance via submitting an 45 | issue. 46 | * If you fix documentation: 47 | * If you plan extensive rewrites/additions to the docs, then 48 | please [contact the maintainers](#contacting-maintainers) to coordinate the work in advance. 49 | 50 | ## Style guides 51 | 52 | A few things to remember: 53 | 54 | * Your code should conform to 55 | the official [Kotlin code style guide](https://kotlinlang.org/docs/reference/coding-conventions.html). 56 | Code style is managed by [EditorConfig](https://www.jetbrains.com/help/idea/editorconfig.html), 57 | so make sure the EditorConfig plugin is enabled in the IDE. 58 | * Every public API (including functions, classes, objects and so on) should be documented, 59 | every parameter, property, return types and exceptions should be described properly. 60 | 61 | ## Commit messages 62 | 63 | * Commit messages should be written in English 64 | * They should be written in present tense using imperative mood 65 | ("Fix" instead of "Fixes", "Improve" instead of "Improved"). 66 | Add the related bug reference to a commit message (bug number after a hash character between round braces). 67 | 68 | See [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/) 69 | 70 | ## Building 71 | 72 | ### Requirements 73 | 74 | * To build MCP Kotlin SDK, JDK version 21 or higher is required. Make sure this is your default JDK (`JAVA_HOME` is set 75 | accordingly) 76 | * The project can be opened in IntelliJ IDEA without additional prerequisites. 77 | 78 | ### Building MCP Kotlin SDK from source 79 | 80 | * Run `./gradlew assemble` to build the project and produce the corresponding artifacts. 81 | * Run `./gradlew test` to test the module and speed up development. 82 | * Run `./gradlew build` to build the project, which also runs all the tests. 83 | 84 | ## Contacting maintainers 85 | 86 | * If something cannot be done, not convenient, or does not work — submit an [issue](#submitting-issues). 87 | * "How to do something" questions — [StackOverflow](https://stackoverflow.com). 88 | * Discussions and general inquiries — use [KotlinLang Slack](https://kotl.in/slack). 89 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Anthropic, PBC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This repository is moved 2 | 3 | The new repo is located here: [https://github.com/modelcontextprotocol/kotlin-sdk](https://github.com/modelcontextprotocol/kotlin-sdk) 4 | 5 | # MCP Kotlin SDK 6 | 7 | Kotlin implementation of the [Model Context Protocol](https://modelcontextprotocol.io) (MCP), providing both client and server capabilities for integrating with LLM surfaces. 8 | 9 | ## Overview 10 | 11 | The Model Context Protocol allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. This Kotlin SDK implements the full MCP specification, making it easy to: 12 | 13 | - Build MCP clients that can connect to any MCP server 14 | - Create MCP servers that expose resources, prompts and tools 15 | - Use standard transports like stdio, SSE, and WebSocket 16 | - Handle all MCP protocol messages and lifecycle events 17 | 18 | ## Samples 19 | 20 | - [kotlin-mcp-server](./samples/kotlin-mcp-server): shows how to set up Kotlin MCP server with different tools and other features. 21 | 22 | ## Installation 23 | 24 | Add the new repository to your build file: 25 | 26 | ```kotlin 27 | repositories { 28 | maven(url = "https://maven.pkg.jetbrains.space/public/p/kotlin-mcp-sdk/sdk") 29 | } 30 | ``` 31 | 32 | Add the dependency: 33 | 34 | ```kotlin 35 | dependencies { 36 | implementation("org.jetbrains.kotlinx:kotlinx-mcp-sdk:0.1.0") 37 | } 38 | ``` 39 | 40 | ## Quick Start 41 | 42 | ### Creating a Client 43 | 44 | ```kotlin 45 | import org.jetbrains.kotlinx.mcp.client.Client 46 | import org.jetbrains.kotlinx.mcp.client.StdioClientTransport 47 | import org.jetbrains.kotlinx.mcp.Implementation 48 | 49 | val client = Client( 50 | clientInfo = Implementation( 51 | name = "example-client", 52 | version = "1.0.0" 53 | ) 54 | ) 55 | 56 | val transport = StdioClientTransport( 57 | inputStream = processInputStream, 58 | outputStream = processOutputStream 59 | ) 60 | 61 | // Connect to server 62 | client.connect(transport) 63 | 64 | // List available resources 65 | val resources = client.listResources() 66 | 67 | // Read a specific resource 68 | val resourceContent = client.readResource( 69 | ReadResourceRequest(uri = "file:///example.txt") 70 | ) 71 | ``` 72 | 73 | ### Creating a Server 74 | 75 | ```kotlin 76 | import org.jetbrains.kotlinx.mcp.server.Server 77 | import org.jetbrains.kotlinx.mcp.server.ServerOptions 78 | import org.jetbrains.kotlinx.mcp.server.StdioServerTransport 79 | import org.jetbrains.kotlinx.mcp.ServerCapabilities 80 | 81 | val server = Server( 82 | serverInfo = Implementation( 83 | name = "example-server", 84 | version = "1.0.0" 85 | ), 86 | options = ServerOptions( 87 | capabilities = ServerCapabilities( 88 | resources = ServerCapabilities.Resources( 89 | subscribe = true, 90 | listChanged = true 91 | ) 92 | ) 93 | ) 94 | ) 95 | 96 | // Add a resource 97 | server.addResource( 98 | uri = "file:///example.txt", 99 | name = "Example Resource", 100 | description = "An example text file", 101 | mimeType = "text/plain" 102 | ) { request -> 103 | ReadResourceResult( 104 | contents = listOf( 105 | TextResourceContents( 106 | text = "This is the content of the example resource.", 107 | uri = request.uri, 108 | mimeType = "text/plain" 109 | ) 110 | ) 111 | ) 112 | } 113 | 114 | // Start server with stdio transport 115 | val transport = StdioServerTransport() 116 | server.connect(transport) 117 | ``` 118 | 119 | ### Using SSE Transport 120 | 121 | ```kotlin 122 | import io.ktor.server.application.* 123 | import org.jetbrains.kotlinx.mcp.server.MCP 124 | 125 | fun Application.module() { 126 | MCP { 127 | Server( 128 | serverInfo = Implementation( 129 | name = "example-sse-server", 130 | version = "1.0.0" 131 | ), 132 | options = ServerOptions( 133 | capabilities = ServerCapabilities( 134 | prompts = ServerCapabilities.Prompts(listChanged = null), 135 | resources = ServerCapabilities.Resources(subscribe = null, listChanged = null) 136 | ) 137 | ) 138 | ) 139 | } 140 | } 141 | ``` 142 | 143 | ## Contributing 144 | 145 | Please see the [contribution guide](CONTRIBUTING.md) and the [Code of conduct](CODE_OF_CONDUCT.md) before contributing. 146 | 147 | ## License 148 | 149 | This project is licensed under the MIT License—see the [LICENSE](LICENSE) file for details. 150 | -------------------------------------------------------------------------------- /docs/-m-c-p -kotlin -s-d-k/org.jetbrains.kotlinx.mcp/-complete-request/ref.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ref 6 | 7 | 8 | 9 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | 61 |
62 | 65 |
66 |
67 | 68 |
69 |

ref

70 |
71 | 72 |
73 | 83 |
84 |
85 |
86 | 87 | -------------------------------------------------------------------------------- /docs/-m-c-p -kotlin -s-d-k/org.jetbrains.kotlinx.mcp/-create-message-result/role.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | role 6 | 7 | 8 | 9 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | 61 |
62 | 65 |
66 |
67 | 68 |
69 |

role

70 |
71 | 72 |
73 | 83 |
84 |
85 |
86 | 87 | -------------------------------------------------------------------------------- /docs/-m-c-p -kotlin -s-d-k/org.jetbrains.kotlinx.mcp/-implementation/name.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | 7 | 8 | 9 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | 61 |
62 | 65 |
66 |
67 | 68 |
69 |

name

70 |
71 | 72 |
73 | 83 |
84 |
85 |
86 | 87 | -------------------------------------------------------------------------------- /docs/-m-c-p -kotlin -s-d-k/org.jetbrains.kotlinx.mcp/-j-s-o-n-r-p-c-error/code.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | code 6 | 7 | 8 | 9 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | 61 |
62 | 65 |
66 |
67 | 68 |
69 |

code

70 |
71 | 72 |
73 | 83 |
84 |
85 |
86 | 87 | -------------------------------------------------------------------------------- /docs/-m-c-p -kotlin -s-d-k/org.jetbrains.kotlinx.mcp/-j-s-o-n-r-p-c-request/id.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | id 6 | 7 | 8 | 9 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | 61 |
62 | 65 |
66 |
67 | 68 |
69 |

id

70 |
71 | 72 |
73 | 83 |
84 |
85 |
86 | 87 | -------------------------------------------------------------------------------- /docs/-m-c-p -kotlin -s-d-k/org.jetbrains.kotlinx.mcp/-j-s-o-n-r-p-c-response/id.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | id 6 | 7 | 8 | 9 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | 61 |
62 | 65 |
66 |
67 | 68 |
69 |

id

70 |
71 | 72 |
73 | 83 |
84 |
85 |
86 | 87 | -------------------------------------------------------------------------------- /docs/-m-c-p -kotlin -s-d-k/org.jetbrains.kotlinx.mcp/-j-s-o-n-r-p-c_-v-e-r-s-i-o-n.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSONRPC_VERSION 6 | 7 | 8 | 9 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | 61 |
62 | 65 |
66 |
67 | 68 |
69 |

JSONRPC_VERSION

70 |
71 | 72 |
73 | 83 |
84 |
85 |
86 | 87 | -------------------------------------------------------------------------------- /docs/-m-c-p -kotlin -s-d-k/org.jetbrains.kotlinx.mcp/-mcp-error/code.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | code 6 | 7 | 8 | 9 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | 61 |
62 | 65 |
66 |
67 | 68 |
69 |

code

70 |
71 | 72 |
73 | 83 |
84 |
85 |
86 | 87 | -------------------------------------------------------------------------------- /docs/-m-c-p -kotlin -s-d-k/org.jetbrains.kotlinx.mcp/-mcp-error/data.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | data 6 | 7 | 8 | 9 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | 61 |
62 | 65 |
66 |
67 | 68 |
69 |

data

70 |
71 |
val data: JsonObject(source)
72 |
73 | 83 |
84 |
85 |
86 | 87 | -------------------------------------------------------------------------------- /docs/-m-c-p -kotlin -s-d-k/org.jetbrains.kotlinx.mcp/-ping-request/-ping-request.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PingRequest 6 | 7 | 8 | 9 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | 61 |
62 | 65 |
66 |
67 | 68 |
69 |

PingRequest

70 |
71 |
constructor()(source)
72 |
73 | 83 |
84 |
85 |
86 | 87 | -------------------------------------------------------------------------------- /docs/-m-c-p -kotlin -s-d-k/org.jetbrains.kotlinx.mcp/-prompt-message/role.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | role 6 | 7 | 8 | 9 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | 61 |
62 | 65 |
66 |
67 | 68 |
69 |

role

70 |
71 | 72 |
73 | 83 |
84 |
85 |
86 | 87 | -------------------------------------------------------------------------------- /docs/-m-c-p -kotlin -s-d-k/org.jetbrains.kotlinx.mcp/-request/method.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | method 6 | 7 | 8 | 9 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | 61 |
62 | 65 |
66 |
67 | 68 |
69 |

method

70 |
71 |
abstract val method: Method(source)
72 |
73 | 83 |
84 |
85 |
86 | 87 | -------------------------------------------------------------------------------- /docs/-m-c-p -kotlin -s-d-k/org.jetbrains.kotlinx.mcp/-sampling-message/role.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | role 6 | 7 | 8 | 9 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | 61 |
62 | 65 |
66 |
67 | 68 |
69 |

role

70 |
71 | 72 |
73 | 83 |
84 |
85 |
86 | 87 | -------------------------------------------------------------------------------- /docs/-m-c-p -kotlin -s-d-k/shared/-l-i-b_-v-e-r-s-i-o-n.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LIB_VERSION 6 | 7 | 8 | 9 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | 61 |
62 | 65 |
66 |
67 | 68 |
69 |

LIB_VERSION

70 |
71 |
const val LIB_VERSION: String
72 |
73 | 83 |
84 |
85 |
86 | 87 | -------------------------------------------------------------------------------- /docs/images/anchor-copy-button.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/images/arrow_down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/images/burger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /docs/images/copy-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/images/copy-successful-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/images/footer-go-to-link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/images/go-to-top-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/images/homepage.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/images/logo-icon.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs/images/nav-icons/abstract-class-kotlin.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /docs/images/nav-icons/abstract-class.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/images/nav-icons/annotation-kotlin.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/images/nav-icons/annotation.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/images/nav-icons/class-kotlin.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/images/nav-icons/class.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/images/nav-icons/enum-kotlin.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/images/nav-icons/enum.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/images/nav-icons/exception-class.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/images/nav-icons/field-value.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs/images/nav-icons/field-variable.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs/images/nav-icons/function.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/images/nav-icons/interface-kotlin.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/images/nav-icons/interface.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/images/nav-icons/object.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/images/nav-icons/typealias-kotlin.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/images/theme-toggle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/scripts/clipboard.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | window.addEventListener('load', () => { 6 | document.querySelectorAll('span.copy-icon').forEach(element => { 7 | element.addEventListener('click', (el) => copyElementsContentToClipboard(element)); 8 | }) 9 | 10 | document.querySelectorAll('span.anchor-icon').forEach(element => { 11 | element.addEventListener('click', (el) => { 12 | if(element.hasAttribute('pointing-to')){ 13 | const location = hrefWithoutCurrentlyUsedAnchor() + '#' + element.getAttribute('pointing-to') 14 | copyTextToClipboard(element, location) 15 | } 16 | }); 17 | }) 18 | }) 19 | 20 | const copyElementsContentToClipboard = (element) => { 21 | const selection = window.getSelection(); 22 | const range = document.createRange(); 23 | range.selectNodeContents(element.parentNode.parentNode); 24 | selection.removeAllRanges(); 25 | selection.addRange(range); 26 | 27 | copyAndShowPopup(element, () => selection.removeAllRanges()) 28 | } 29 | 30 | const copyTextToClipboard = (element, text) => { 31 | var textarea = document.createElement("textarea"); 32 | textarea.textContent = text; 33 | textarea.style.position = "fixed"; 34 | document.body.appendChild(textarea); 35 | textarea.select(); 36 | 37 | copyAndShowPopup(element, () => document.body.removeChild(textarea)) 38 | } 39 | 40 | const copyAndShowPopup = (element, after) => { 41 | try { 42 | document.execCommand('copy'); 43 | element.nextElementSibling.classList.add('active-popup'); 44 | setTimeout(() => { 45 | element.nextElementSibling.classList.remove('active-popup'); 46 | }, 1200); 47 | } catch (e) { 48 | console.error('Failed to write to clipboard:', e) 49 | } 50 | finally { 51 | if(after) after() 52 | } 53 | } 54 | 55 | const hrefWithoutCurrentlyUsedAnchor = () => window.location.href.split('#')[0] 56 | 57 | -------------------------------------------------------------------------------- /docs/scripts/navigation-loader.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | navigationPageText = fetch(pathToRoot + "navigation.html").then(response => response.text()) 6 | 7 | displayNavigationFromPage = () => { 8 | navigationPageText.then(data => { 9 | document.getElementById("sideMenu").innerHTML = data; 10 | }).then(() => { 11 | document.querySelectorAll(".overview > a").forEach(link => { 12 | link.setAttribute("href", pathToRoot + link.getAttribute("href")); 13 | }) 14 | }).then(() => { 15 | document.querySelectorAll(".sideMenuPart").forEach(nav => { 16 | if (!nav.classList.contains("hidden")) 17 | nav.classList.add("hidden") 18 | }) 19 | }).then(() => { 20 | revealNavigationForCurrentPage() 21 | }).then(() => { 22 | scrollNavigationToSelectedElement() 23 | }) 24 | document.querySelectorAll('.footer a[href^="#"]').forEach(anchor => { 25 | anchor.addEventListener('click', function (e) { 26 | e.preventDefault(); 27 | document.querySelector(this.getAttribute('href')).scrollIntoView({ 28 | behavior: 'smooth' 29 | }); 30 | }); 31 | }); 32 | } 33 | 34 | revealNavigationForCurrentPage = () => { 35 | let pageId = document.getElementById("content").attributes["pageIds"].value.toString(); 36 | let parts = document.querySelectorAll(".sideMenuPart"); 37 | let found = 0; 38 | do { 39 | parts.forEach(part => { 40 | if (part.attributes['pageId'].value.indexOf(pageId) !== -1 && found === 0) { 41 | found = 1; 42 | if (part.classList.contains("hidden")) { 43 | part.classList.remove("hidden"); 44 | part.setAttribute('data-active', ""); 45 | } 46 | revealParents(part) 47 | } 48 | }); 49 | pageId = pageId.substring(0, pageId.lastIndexOf("/")) 50 | } while (pageId.indexOf("/") !== -1 && found === 0) 51 | }; 52 | revealParents = (part) => { 53 | if (part.classList.contains("sideMenuPart")) { 54 | if (part.classList.contains("hidden")) 55 | part.classList.remove("hidden"); 56 | revealParents(part.parentNode) 57 | } 58 | }; 59 | 60 | scrollNavigationToSelectedElement = () => { 61 | let selectedElement = document.querySelector('div.sideMenuPart[data-active]') 62 | if (selectedElement == null) { // nothing selected, probably just the main page opened 63 | return 64 | } 65 | 66 | let hasIcon = selectedElement.querySelectorAll(":scope > div.overview span.nav-icon").length > 0 67 | 68 | // for instance enums also have children and are expandable, but are not package/module elements 69 | let isPackageElement = selectedElement.children.length > 1 && !hasIcon 70 | if (isPackageElement) { 71 | // if package is selected or linked, it makes sense to align it to top 72 | // so that you can see all the members it contains 73 | selectedElement.scrollIntoView(true) 74 | } else { 75 | // if a member within a package is linked, it makes sense to center it since it, 76 | // this should make it easier to look at surrounding members 77 | selectedElement.scrollIntoView({ 78 | behavior: 'auto', 79 | block: 'center', 80 | inline: 'center' 81 | }) 82 | } 83 | } 84 | 85 | /* 86 | This is a work-around for safari being IE of our times. 87 | It doesn't fire a DOMContentLoaded, presumabely because eventListener is added after it wants to do it 88 | */ 89 | if (document.readyState == 'loading') { 90 | window.addEventListener('DOMContentLoaded', () => { 91 | displayNavigationFromPage() 92 | }) 93 | } else { 94 | displayNavigationFromPage() 95 | } 96 | -------------------------------------------------------------------------------- /docs/scripts/sourceset_dependencies.js: -------------------------------------------------------------------------------- 1 | sourceset_dependencies='{":/main":[]}' -------------------------------------------------------------------------------- /docs/scripts/symbol-parameters-wrapper_deferred.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | // helps with some corner cases where starts working already, 6 | // but the signature is not yet long enough to be wrapped 7 | (function() { 8 | const leftPaddingPx = 60; 9 | 10 | function createNbspIndent() { 11 | let indent = document.createElement("span"); 12 | indent.append(document.createTextNode("\u00A0\u00A0\u00A0\u00A0")); 13 | indent.classList.add("nbsp-indent"); 14 | return indent; 15 | } 16 | 17 | function wrapSymbolParameters(entry) { 18 | const symbol = entry.target; 19 | const symbolBlockWidth = entry.borderBoxSize && entry.borderBoxSize[0] && entry.borderBoxSize[0].inlineSize; 20 | 21 | // Even though the script is marked as `defer` and we wait for `DOMContentLoaded` event, 22 | // or if this block is a part of hidden tab, it can happen that `symbolBlockWidth` is 0, 23 | // indicating that something hasn't been loaded. 24 | // In this case, observer will be triggered onсe again when it will be ready. 25 | if (symbolBlockWidth > 0) { 26 | const node = symbol.querySelector(".parameters"); 27 | 28 | if (node) { 29 | // if window resize happened and observer was triggered, reset previously wrapped 30 | // parameters as they might not need wrapping anymore, and check again 31 | node.classList.remove("wrapped"); 32 | node.querySelectorAll(".parameter .nbsp-indent") 33 | .forEach(indent => indent.remove()); 34 | 35 | const innerTextWidth = Array.from(symbol.children) 36 | .filter(it => !it.classList.contains("block")) // blocks are usually on their own (like annotations), so ignore it 37 | .map(it => it.getBoundingClientRect().width) 38 | .reduce((a, b) => a + b, 0); 39 | 40 | // if signature text takes up more than a single line, wrap params for readability 41 | if (innerTextWidth > (symbolBlockWidth - leftPaddingPx)) { 42 | node.classList.add("wrapped"); 43 | node.querySelectorAll(".parameter").forEach(param => { 44 | // has to be a physical indent so that it can be copied. styles like 45 | // paddings and `::before { content: " " }` do not work for that 46 | param.prepend(createNbspIndent()); 47 | }); 48 | } 49 | } 50 | } 51 | } 52 | 53 | const symbolsObserver = new ResizeObserver(entries => entries.forEach(wrapSymbolParameters)); 54 | 55 | function initHandlers() { 56 | document.querySelectorAll("div.symbol").forEach(symbol => symbolsObserver.observe(symbol)); 57 | } 58 | 59 | if (document.readyState === 'loading') window.addEventListener('DOMContentLoaded', initHandlers); 60 | else initHandlers(); 61 | 62 | // ToDo: Add `unobserve` if dokka will be SPA-like: 63 | // https://github.com/w3c/csswg-drafts/issues/5155 64 | })(); 65 | -------------------------------------------------------------------------------- /docs/styles/font-jb-sans-auto.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | /* Light weight */ 6 | @font-face { 7 | font-family: 'JetBrains Sans'; 8 | src: url('https://resources.jetbrains.com/storage/jetbrains-sans/JetBrainsSans-Light.woff2') format('woff2'), url('https://resources.jetbrains.com/storage/jetbrains-sans/JetBrainsSans-Light.woff') format('woff'); 9 | font-weight: 300; 10 | font-style: normal; 11 | } 12 | /* Regular weight */ 13 | @font-face { 14 | font-family: 'JetBrains Sans'; 15 | src: url('https://resources.jetbrains.com/storage/jetbrains-sans/JetBrainsSans-Regular.woff2') format('woff2'), url('https://resources.jetbrains.com/storage/jetbrains-sans/JetBrainsSans-Regular.woff') format('woff'); 16 | font-weight: 400; 17 | font-style: normal; 18 | } 19 | /* SemiBold weight */ 20 | @font-face { 21 | font-family: 'JetBrains Sans'; 22 | src: url('https://resources.jetbrains.com/storage/jetbrains-sans/JetBrainsSans-SemiBold.woff2') format('woff2'), url('https://resources.jetbrains.com/storage/jetbrains-sans/JetBrainsSans-SemiBold.woff') format('woff'); 23 | font-weight: 600; 24 | font-style: normal; 25 | } 26 | 27 | @supports (font-variation-settings: normal) { 28 | @font-face { 29 | font-family: 'JetBrains Sans'; 30 | src: url('https://resources.jetbrains.com/storage/jetbrains-sans/JetBrainsSans.woff2') format('woff2 supports variations'), 31 | url('https://resources.jetbrains.com/storage/jetbrains-sans/JetBrainsSans.woff2') format('woff2-variations'), 32 | url('https://resources.jetbrains.com/storage/jetbrains-sans/JetBrainsSans.woff') format('woff-variations'); 33 | font-weight: 100 900; 34 | font-style: normal; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /docs/styles/logo-styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | :root { 6 | --dokka-logo-image-url: url('../images/logo-icon.svg'); 7 | --dokka-logo-height: 50px; 8 | --dokka-logo-width: 50px; 9 | } 10 | -------------------------------------------------------------------------------- /docs/styles/prism.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Custom Dokka styles 3 | */ 4 | code .token { 5 | white-space: pre; 6 | } 7 | 8 | /** 9 | * Styles based on webhelp's prism.js styles 10 | * Changes: 11 | * - Since webhelp's styles are in .pcss, they use nesting which is not achievable in native CSS 12 | * so nested css blocks have been unrolled (like dark theme). 13 | * - Webhelp uses "Custom Class" prism.js plugin, so all of their prism classes are prefixed with "--prism". 14 | * Dokka doesn't seem to need this plugin at the moment, so all "--prism" prefixes have been removed. 15 | * - Removed all styles related to `pre` and `code` tags. Kotlinlang's resulting styles are so spread out and complicated 16 | * that it's difficult to gather in one place. Instead use code styles defined in the main Dokka styles, 17 | * which at the moment looks fairly similar. 18 | * 19 | * Based on prism.js default theme 20 | * Based on dabblet (http://dabblet.com) 21 | * @author Lea Verou 22 | */ 23 | 24 | .token.comment, 25 | .token.prolog, 26 | .token.doctype, 27 | .token.cdata { 28 | color: #8c8c8c; 29 | } 30 | 31 | .token.punctuation { 32 | color: #999; 33 | } 34 | 35 | .token.namespace { 36 | opacity: 0.7; 37 | } 38 | 39 | .token.property, 40 | .token.tag, 41 | .token.boolean, 42 | .token.number, 43 | .token.constant, 44 | .token.symbol, 45 | .token.deleted { 46 | color: #871094; 47 | } 48 | 49 | .token.selector, 50 | .token.attr-name, 51 | .token.string, 52 | .token.char, 53 | .token.builtin, 54 | .token.inserted { 55 | color: #067d17; 56 | } 57 | 58 | .token.operator, 59 | .token.entity, 60 | .token.url, 61 | .language-css .token.string, 62 | .style .token.string { 63 | color: #9a6e3a; 64 | /* This background color was intended by the author of this theme. */ 65 | background: hsla(0, 0%, 100%, 0.5); 66 | } 67 | 68 | .token.atrule, 69 | .token.attr-value, 70 | .token.keyword { 71 | font-size: inherit; /* to override .keyword */ 72 | color: #0033b3; 73 | } 74 | 75 | .token.function { 76 | color: #00627a; 77 | } 78 | 79 | .token.class-name { 80 | color: #000000; 81 | } 82 | 83 | .token.regex, 84 | .token.important, 85 | .token.variable { 86 | color: #871094; 87 | } 88 | 89 | .token.important, 90 | .token.bold { 91 | font-weight: bold; 92 | } 93 | .token.italic { 94 | font-style: italic; 95 | } 96 | 97 | .token.entity { 98 | cursor: help; 99 | } 100 | 101 | .token.operator { 102 | background: none; 103 | } 104 | 105 | /* 106 | * DARK THEME 107 | */ 108 | :root.theme-dark .token.comment, 109 | :root.theme-dark .token.prolog, 110 | :root.theme-dark .token.cdata { 111 | color: #808080; 112 | } 113 | 114 | :root.theme-dark .token.delimiter, 115 | :root.theme-dark .token.boolean, 116 | :root.theme-dark .token.keyword, 117 | :root.theme-dark .token.selector, 118 | :root.theme-dark .token.important, 119 | :root.theme-dark .token.atrule { 120 | color: #cc7832; 121 | } 122 | 123 | :root.theme-dark .token.operator, 124 | :root.theme-dark .token.punctuation, 125 | :root.theme-dark .token.attr-name { 126 | color: #a9b7c6; 127 | } 128 | 129 | :root.theme-dark .token.tag, 130 | :root.theme-dark .token.tag .punctuation, 131 | :root.theme-dark .token.doctype, 132 | :root.theme-dark .token.builtin { 133 | color: #e8bf6a; 134 | } 135 | 136 | :root.theme-dark .token.entity, 137 | :root.theme-dark .token.number, 138 | :root.theme-dark .token.symbol { 139 | color: #6897bb; 140 | } 141 | 142 | :root.theme-dark .token.property, 143 | :root.theme-dark .token.constant, 144 | :root.theme-dark .token.variable { 145 | color: #9876aa; 146 | } 147 | 148 | :root.theme-dark .token.string, 149 | :root.theme-dark .token.char { 150 | color: #6a8759; 151 | } 152 | 153 | :root.theme-dark .token.attr-value, 154 | :root.theme-dark .token.attr-value .punctuation { 155 | color: #a5c261; 156 | } 157 | 158 | :root.theme-dark .token.attr-value .punctuation:first-child { 159 | color: #a9b7c6; 160 | } 161 | 162 | :root.theme-dark .token.url { 163 | text-decoration: underline; 164 | 165 | color: #287bde; 166 | background: transparent; 167 | } 168 | 169 | :root.theme-dark .token.function { 170 | color: #ffc66d; 171 | } 172 | 173 | :root.theme-dark .token.regex { 174 | background: #364135; 175 | } 176 | 177 | :root.theme-dark .token.deleted { 178 | background: #484a4a; 179 | } 180 | 181 | :root.theme-dark .token.inserted { 182 | background: #294436; 183 | } 184 | 185 | :root.theme-dark .token.class-name { 186 | color: #a9b7c6; 187 | } 188 | 189 | :root.theme-dark .token.function { 190 | color: #ffc66d; 191 | } 192 | 193 | :root.theme-darkcode .language-css .token.property, 194 | :root.theme-darkcode .language-css, 195 | :root.theme-dark .token.property + .token.punctuation { 196 | color: #a9b7c6; 197 | } 198 | 199 | code.language-css .token.id { 200 | color: #ffc66d; 201 | } 202 | 203 | :root.theme-dark code.language-css .token.selector > .token.class, 204 | :root.theme-dark code.language-css .token.selector > .token.attribute, 205 | :root.theme-dark code.language-css .token.selector > .token.pseudo-class, 206 | :root.theme-dark code.language-css .token.selector > .token.pseudo-element { 207 | color: #ffc66d; 208 | } 209 | 210 | :root.theme-dark .language-plaintext .token { 211 | /* plaintext code should be colored as article text */ 212 | color: inherit !important; 213 | } 214 | -------------------------------------------------------------------------------- /docs/ui-kit/ui-kit.min.css: -------------------------------------------------------------------------------- 1 | :root{--breakpoint-desktop-min:900px;--breakpoint-tablet-max:899px;--breakpoint-tablet-min:440px;--breakpoint-mobile-max:439px;--breakpoint-mobile-min:360px;--color-key-blue:#307fff;--color-nav-bar:#27282c;--color-dark-theme-bg:#262628;--color-text-black:#000;--color-text-white:hsla(0,0%,100%,.96);--color-text-light-black:rgba(0,0,0,.7);--color-text-light-white:hsla(0,0%,100%,.7);--color-w05:hsla(0,0%,100%,.05);--color-w10:hsla(0,0%,100%,.1);--color-w20:hsla(0,0%,100%,.2);--color-w50:hsla(0,0%,100%,.5);--color-w70:hsla(0,0%,100%,.7);--color-w80:hsla(0,0%,100%,.8);--color-b05:rgba(0,0,0,.05);--color-b08:rgba(0,0,0,.08);--color-b20:rgba(0,0,0,.2);--color-b50:rgba(0,0,0,.5);--color-b70:rgba(0,0,0,.7);--color-cd-punctuation:#999;--color-cd-keyword:#0033b3;--color-cd-keyword-alternative:#cc7832;--color-cd-builtin:#067d17;--color-cd-builtin-alternative:#e7bf6a;--color-cd-function:#00627a;--color-cd-function-alternative:#ffc66d;--color-cd-operator:#9a6e3a;--color-cd-operator-alternative:#a9b7c6;--color-cd-body:#000;--color-cd-body-alternative:#a9b7c6;--color-generic:#539df3;--color-jvm:#4dbb5f;--color-js:#ffc700;--color-wasm:#fff;--size-s1:4px;--size-s2:8px;--size-s3:16px;--size-m1:24px;--size-m2:32px;--size-m3:48px;--size-l1:64px;--size-l2:72px;--size-ta1:40px;--size-ta2:52px;--font-family-default:JetBrains Sans,Inter,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Droid Sans,Helvetica Neue,Arial,sans-serif;--font-family-mono:JetBrains Mono,SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;--font-h1:600 44px/44px var(--font-family-default);--font-h2:600 32px/32px var(--font-family-default);--font-h3:600 24px/24px var(--font-family-default);--font-h4:600 16px/24px var(--font-family-default);--font-text-m:400 16px/24px var(--font-family-default);--font-text-s:400 13px/24px var(--font-family-default);--font-code:400 16px/24px var(--font-family-mono)}.platform-hinted>.platform-bookmarks-row,.tabs-section{margin-left:-8px;margin-right:-8px}.platform-hinted>.platform-bookmarks-row>.platform-bookmark,.section-tab{background-color:transparent;border:0;border-bottom:1px solid var(--inactive-tab-border-color);color:var(--inactive-section-color);cursor:pointer;font-size:var(--default-font-size);margin:0 8px;outline:none;padding:11px 3px}.platform-hinted>.platform-bookmarks-row{margin-bottom:16px;margin-bottom:var(--size-s3)}.no-js .platform-bookmarks-row+.sourceset-dependent-content{margin-top:8px;margin-top:var(--size-s2)}.no-js .platform-bookmarks-row+.sourceset-dependent-content:last-of-type{margin-top:0}.section-tab:hover{border-bottom:2px solid var(--default-font-color);color:var(--default-font-color)}.section-tab[data-active=""]{border-bottom:2px solid var(--active-tab-border-color);color:var(--active-section-color)}.tabs-section-body>div{margin-top:12px}.tabs-section-body .with-platform-tabs{padding-bottom:12px;padding-top:12px}.platform-hinted{display:block;flex:auto}.platform-hinted>.platform-bookmarks-row>.platform-bookmark{align-self:flex-start;background:inherit;flex:none;min-width:64px;order:5}.platform-hinted>.platform-bookmarks-row>.platform-bookmark:hover{border-bottom:2px solid var(--default-font-color);color:var(--default-font-color)}.platform-hinted>.platform-bookmarks-row>.platform-bookmark[data-active=""]{border-bottom:2px solid var(--active-tab-border-color);color:var(--active-section-color)}.js .platform-hinted>.content:not([data-active]),.js .tabs-section-body [data-togglable]:not([data-active]),.main-content[data-page-type=package] .tabs-section-body h2,.no-js .platform-bookmarks-row,.no-js .tabs-section{display:none} 2 | -------------------------------------------------------------------------------- /docs/ui-kit/ui-kit.min.js: -------------------------------------------------------------------------------- 1 | (()=>{"use strict";var t=function(t){var e="function"==typeof Symbol&&Symbol.iterator,a=e&&t[e],r=0;if(a)return a.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&r>=t.length&&(t=void 0),{value:t&&t[r++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")},e=function(){var t=!1;try{var e="__testLocalStorageKey__";localStorage.setItem(e,e),localStorage.removeItem(e),t=!0}catch(t){console.error("Local storage is not available",t)}return{getItem:function(e){return t?localStorage.getItem(e):null},setItem:function(e,a){t&&localStorage.setItem(e,a)}}}();function a(e){var a,r=null===(a=e.getAttribute("data-togglable"))||void 0===a?void 0:a.split(",");!function(a){var r,o,n,l;try{for(var i=t(document.getElementsByClassName("tabs-section")),c=i.next();!c.done;c=i.next()){var u=c.value;try{for(var d=(n=void 0,t(u.children)),s=d.next();!s.done;s=d.next()){var g=s.value;g.getAttribute("data-togglable")===e.getAttribute("data-togglable")?g.setAttribute("data-active",""):g.removeAttribute("data-active")}}catch(t){n={error:t}}finally{try{s&&!s.done&&(l=d.return)&&l.call(d)}finally{if(n)throw n.error}}}}catch(t){r={error:t}}finally{try{c&&!c.done&&(o=i.return)&&o.call(i)}finally{if(r)throw r.error}}}(),document.querySelectorAll(".tabs-section-body *[data-togglable]").forEach((function(t){var e=t.getAttribute("data-togglable");r&&e&&r.includes(e)?t.setAttribute("data-active",""):t.classList.contains("sourceset-dependent-content")||t.removeAttribute("data-active")}))}window.initTabs=function(){var t=document.querySelector(".main-content"),r="active-tab-"+(t?t.getAttribute("data-page-type"):null);document.querySelectorAll("div[tabs-section]").forEach((function(t){!function(t){var e=t.querySelector("button[data-active]");e&&a(e)}(t),t.addEventListener("click",(function(t){var o=t.target,n=o?o.getAttribute("data-togglable"):null;n&&(e.setItem(r,JSON.stringify(n)),a(o))}))}));var o=e.getItem(r);if(o){var n=document.querySelector('div[tabs-section] > button[data-togglable="'+JSON.parse(o)+'"]');n&&a(n)}},window.toggleSections=a})(); -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | 3 | org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled 4 | org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true 5 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | # plugins version 3 | kotlin = "2.0.21" 4 | dokka = "2.0.0-Beta" 5 | 6 | # libraries version 7 | serialization = "1.7.3" 8 | coroutines = "1.9.0" 9 | ktor = "3.0.2" 10 | mockk = "1.13.13" 11 | logging = "7.0.0" 12 | 13 | [libraries] 14 | # Kotlinx libraries 15 | kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" } 16 | kotlin-logging = { group = "io.github.oshai", name = "kotlin-logging-jvm", version.ref = "logging" } 17 | 18 | # Ktor 19 | ktor-client-apache = { group = "io.ktor", name = "ktor-client-apache", version.ref = "ktor" } 20 | ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor" } 21 | ktor-server-sse = { group = "io.ktor", name = "ktor-server-sse", version.ref = "ktor" } 22 | ktor-server-websockets = { group = "io.ktor", name = "ktor-server-websockets", version.ref = "ktor" } 23 | ktor-server-cio = { group = "io.ktor", name = "ktor-server-cio", version.ref = "ktor" } 24 | 25 | # Testing 26 | kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } 27 | kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" } 28 | kotlinx-coroutines-debug = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-debug", version.ref = "coroutines" } 29 | ktor-server-test-host = { group = "io.ktor", name = "ktor-server-test-host", version.ref = "ktor" } 30 | mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } 31 | 32 | 33 | 34 | [plugins] 35 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 36 | kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 37 | dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/mcp-kotlin-sdk/ceffdfc1b7c394743f65e18a0ef604338521f6cc/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Dec 12 15:35:46 CET 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /samples/connect-to-intellij.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ../gradlew clean localJar; java -jar ../build/libs/kotlinx-mcp-sdk.jar --demo 4 | -------------------------------------------------------------------------------- /samples/inspector.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ../gradlew clean localJar && npx @modelcontextprotocol/inspector java -jar ../build/libs/kotlinx-mcp-sdk.jar --server 4 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-server/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/modules.xml 9 | .idea/jarRepositories.xml 10 | .idea/compiler.xml 11 | .idea/libraries/ 12 | *.iws 13 | *.iml 14 | *.ipr 15 | out/ 16 | !**/src/main/**/out/ 17 | !**/src/test/**/out/ 18 | 19 | ### Kotlin ### 20 | .kotlin 21 | 22 | ### Eclipse ### 23 | .apt_generated 24 | .classpath 25 | .factorypath 26 | .project 27 | .settings 28 | .springBeans 29 | .sts4-cache 30 | bin/ 31 | !**/src/main/**/bin/ 32 | !**/src/test/**/bin/ 33 | 34 | ### NetBeans ### 35 | /nbproject/private/ 36 | /nbbuild/ 37 | /dist/ 38 | /nbdist/ 39 | /.nb-gradle/ 40 | 41 | ### VS Code ### 42 | .vscode/ 43 | 44 | ### Mac OS ### 45 | .DS_Store -------------------------------------------------------------------------------- /samples/kotlin-mcp-server/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-server/.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-server/.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-server/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-server/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-server/README.md: -------------------------------------------------------------------------------- 1 | # MCP Kotlin Server Sample 2 | 3 | A sample implementation of an MCP (Model Communication Protocol) server in Kotlin that demonstrates different server configurations and transport methods. 4 | 5 | ## Features 6 | 7 | - Multiple server operation modes: 8 | - Standard I/O server 9 | - SSE (Server-Sent Events) server with plain configuration 10 | - SSE server using Ktor plugin 11 | - Built-in capabilities for: 12 | - Prompts management 13 | - Resources handling 14 | - Tools integration 15 | 16 | ## Getting Started 17 | 18 | ### Running the Server 19 | 20 | To run the server in SSE mode on the port 3001, run: 21 | 22 | ```bash 23 | ./gradlew run 24 | ``` 25 | 26 | ### Connecting to the Server 27 | 28 | For SSE servers (both plain and Ktor plugin versions): 29 | 1. Start the server 30 | 2. Use the MCP inspector to connect to `http://localhost:/sse` 31 | 32 | ## Server Capabilities 33 | 34 | - **Prompts**: Supports prompt management with list change notifications 35 | - **Resources**: Includes subscription support and list change notifications 36 | - **Tools**: Supports tool management with list change notifications 37 | 38 | ## Implementation Details 39 | 40 | The server is implemented using: 41 | - Ktor for HTTP server functionality 42 | - Kotlin coroutines for asynchronous operations 43 | - SSE for real-time communication 44 | - Standard I/O for command-line interface 45 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-server/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") version "2.1.0" 3 | kotlin("plugin.serialization") version "2.1.0" 4 | application 5 | } 6 | 7 | application { 8 | mainClass.set("MainKt") 9 | } 10 | 11 | 12 | group = "org.example" 13 | version = "0.1.0" 14 | 15 | dependencies { 16 | implementation("org.jetbrains.kotlinx:kotlinx-mcp-sdk:0.1.0") 17 | implementation("org.slf4j:slf4j-nop:2.0.9") 18 | 19 | testImplementation(kotlin("test")) 20 | } 21 | 22 | tasks.test { 23 | useJUnitPlatform() 24 | } 25 | kotlin { 26 | jvmToolchain(21) 27 | } 28 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-server/gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/mcp-kotlin-sdk/ceffdfc1b7c394743f65e18a0ef604338521f6cc/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Dec 16 12:27:09 CET 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-server/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-server/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" 3 | } 4 | rootProject.name = "kotlin-mcp-server" 5 | 6 | dependencyResolutionManagement { 7 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 8 | repositories { 9 | mavenCentral() 10 | maven("https://maven.pkg.jetbrains.space/public/p/kotlin-mcp-sdk/sdk") 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /samples/node-server/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | pnpm build -------------------------------------------------------------------------------- /samples/node-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "dist/server.js", 3 | "type": "module", 4 | "bin": { 5 | "node-server": "dist/server.js" 6 | }, 7 | "files": [ 8 | "dist" 9 | ], 10 | "scripts": { 11 | "build": "tsc --outDir dist && shx chmod +x dist/*.js", 12 | "prepare": "npm run build", 13 | "watch": "tsc --watch --outDir dist", 14 | "run_server": "dist/server.js", 15 | "run_client": "dist/client.js" 16 | }, 17 | "dependencies": { 18 | "@modelcontextprotocol/sdk": "1.0.3", 19 | "node-fetch": "^3.3.2" 20 | }, 21 | "devDependencies": { 22 | "@types/node": "^20.11.0", 23 | "shx": "^0.3.4", 24 | "typescript": "^5.3.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /samples/node-server/run_client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ./dist/client.js -------------------------------------------------------------------------------- /samples/node-server/run_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ./dist/server.js -------------------------------------------------------------------------------- /samples/node-server/src/client.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as net from 'net'; 4 | import { Readable, Writable } from 'stream'; 5 | 6 | const client = new net.Socket(); 7 | const PORT = 3000; 8 | const HOST = '127.0.0.1'; 9 | 10 | // If startClient is supposed to be asynchronous and do something later, 11 | // keep async. Otherwise, remove async if not needed. 12 | function startClient(input: Readable, output: Writable) { 13 | // TODO: Implement client logic here if needed 14 | // For now, it's just a placeholder. 15 | } 16 | 17 | const input = new Readable({ 18 | read() { 19 | // No-op: We'll push data into this stream externally. 20 | } 21 | }); 22 | 23 | const output = new Writable({ 24 | write(chunk, encoding, callback) { 25 | console.log('Writing to output (and pushing to input):', chunk.toString('utf-8')); 26 | input.push(chunk); 27 | callback(); 28 | } 29 | }); 30 | 31 | client.connect(PORT, HOST, () => { 32 | console.log('Connected to server'); 33 | startClient(input, output); 34 | }); 35 | 36 | client.on('data', (data) => { 37 | // When we receive data from the server socket, push it into `input`. 38 | input.push(data); 39 | }); 40 | 41 | client.on('close', () => { 42 | input.push(null); // Signal the end of the input stream 43 | console.log('Connection closed by server'); 44 | }); 45 | 46 | client.on('error', (err) => { 47 | console.error('Client error:', err); 48 | }); 49 | 50 | // Push initial data into `input`. If no reader is attached, this might not be visible. 51 | // Consider adding `input.on('data', ...)` somewhere else if you want to see this data. 52 | input.push("Hello"); -------------------------------------------------------------------------------- /samples/node-server/src/server.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as net from 'net'; 4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 5 | import { Readable, Writable, WritableOptions } from 'stream'; 6 | import { Server } from '@modelcontextprotocol/sdk/server/index.js'; 7 | import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; 8 | 9 | const PORT = 3000; 10 | 11 | const mcpServer = new Server( 12 | { 13 | name: "jetbrains/proxy", 14 | version: "0.1.0", 15 | }, 16 | { 17 | capabilities: { 18 | tools: { 19 | listChanged: true, 20 | }, 21 | resources: {}, 22 | }, 23 | }, 24 | ); 25 | 26 | mcpServer.setRequestHandler(ListToolsRequestSchema, async () => { 27 | console.log("ListToolsRequest received"); 28 | return { 29 | tools: [] 30 | }; 31 | }); 32 | 33 | async function handleConnection(stdin: Readable, stdout: Writable) { 34 | const transport = new StdioServerTransport(stdin, stdout); 35 | try { 36 | await mcpServer.connect(transport); 37 | } catch (err) { 38 | console.error('Error connecting MCP server:', err); 39 | } 40 | } 41 | 42 | // A Writable stream class that writes data back to the socket 43 | class SocketWritable extends Writable { 44 | private socket: net.Socket; 45 | 46 | constructor(socket: net.Socket, options?: WritableOptions) { 47 | super(options); 48 | this.socket = socket; 49 | } 50 | 51 | _write(chunk: any, encoding: BufferEncoding, callback: (error?: Error | null) => void) { 52 | console.log("Socket write:", chunk.toString()); 53 | this.socket.write(chunk, encoding, callback); 54 | } 55 | } 56 | 57 | const server = net.createServer((socket) => { 58 | console.log('Client connected'); 59 | 60 | // Create a Readable stream that we will push data into from the socket 61 | const stdin = new Readable({ 62 | read() { 63 | // We'll use stdin.push() from socket data events 64 | }, 65 | }); 66 | 67 | // Create a Writable that writes back to the socket 68 | const stdout = new SocketWritable(socket); 69 | 70 | socket.on('data', (chunk) => { 71 | const decoded = chunk.toString("utf-8"); 72 | console.log("Socket data:", decoded); 73 | // Push the raw buffer into stdin so the server can read it 74 | stdin.push(chunk); 75 | }); 76 | 77 | socket.on('end', () => { 78 | stdin.push(null); // Signal EOF to the stdin stream 79 | console.log('Client disconnected'); 80 | }); 81 | 82 | socket.on('error', (err) => { 83 | console.error('Socket error:', err); 84 | }); 85 | 86 | handleConnection(stdin, stdout); 87 | }); 88 | 89 | server.listen(PORT, () => { 90 | console.log(`MCP proxy server listening on port ${PORT}`); 91 | }); 92 | 93 | server.on('error', (err) => { 94 | console.error('Server error:', err); 95 | }); -------------------------------------------------------------------------------- /samples/node-server/src/tmp.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 4 | import { 5 | CallToolRequestSchema, 6 | CallToolResult, 7 | ListToolsRequestSchema, 8 | } from "@modelcontextprotocol/sdk/types.js"; 9 | 10 | interface IDEResponseOk { 11 | status: string; 12 | error: null; 13 | } 14 | 15 | interface IDEResponseErr { 16 | status: null; 17 | error: string; 18 | } 19 | 20 | type IDEResponse = IDEResponseOk | IDEResponseErr; 21 | 22 | /** 23 | * Try to find a working IDE endpoint. 24 | * Logic: 25 | * 1. If process.env.IDE_PORT is set, use that port directly. 26 | * 2. If not set, try ports from 63342 to 63352. 27 | * 3. For each port, send a test request to /mcp/list_tools. If it works (res.ok), use that port. 28 | * 4. If no port works, throw an error. 29 | */ 30 | async function findWorkingIDEEndpoint(): Promise { 31 | // If user specified a port, just use that 32 | if (process.env.IDE_PORT) { 33 | const testEndpoint = `http://localhost:${process.env.IDE_PORT}/api`; 34 | if (await testListTools(testEndpoint)) { 35 | return testEndpoint; 36 | } else { 37 | throw new Error(`Specified IDE_PORT=${process.env.IDE_PORT} but it is not responding correctly.`); 38 | } 39 | } 40 | 41 | for (let port = 63342; port <= 63352; port++) { 42 | const candidateEndpoint = `http://localhost:${port}/api`; 43 | if (await testListTools(candidateEndpoint)) { 44 | return candidateEndpoint; 45 | } 46 | } 47 | sendToolsChanged() 48 | previousResponse = "" 49 | throw new Error("No working IDE endpoint found in range 63342-63352"); 50 | } 51 | 52 | let previousResponse: string | null = null; 53 | 54 | function sendToolsChanged() { 55 | try { 56 | server.notification({ method: "notifications/tools/list_changed" }) 57 | } catch (_) { 58 | } 59 | } 60 | 61 | async function testListTools(endpoint: string): Promise { 62 | try { 63 | const res = await fetch(`${endpoint}/mcp/list_tools`); 64 | 65 | if (!res.ok) { 66 | return false; 67 | } 68 | 69 | const currentResponse = await res.text(); 70 | if (previousResponse !== null && previousResponse !== currentResponse) { 71 | sendToolsChanged() 72 | } 73 | previousResponse = currentResponse; 74 | return true; 75 | } catch { 76 | return false; 77 | } 78 | } 79 | 80 | 81 | const server = new Server( 82 | { 83 | name: "jetbrains/proxy", 84 | version: "0.1.0", 85 | }, 86 | { 87 | capabilities: { 88 | tools: { 89 | listChanged: true, 90 | }, 91 | resources: {}, 92 | }, 93 | }, 94 | ); 95 | 96 | server.setRequestHandler(ListToolsRequestSchema, async () => { 97 | const endpoint = await findWorkingIDEEndpoint(); 98 | return { 99 | tools: await fetch(`${endpoint}/mcp/list_tools`) 100 | .then(res => res.ok ? res.json() : Promise.reject(new Error("Unable to list tools"))) 101 | } 102 | }); 103 | 104 | async function handleToolCall(name: string, args: any): Promise { 105 | try { 106 | const endPoint = await findWorkingIDEEndpoint(); 107 | const response = await fetch(`${endPoint}/mcp/${name}`, { 108 | method: 'POST', 109 | headers: { 110 | "Content-Type": "application/json", 111 | }, 112 | body: JSON.stringify(args) 113 | }); 114 | 115 | if (!response.ok) { 116 | throw new Error(`Response failed: ${response.status}`); 117 | } 118 | 119 | const { status, error }: IDEResponse = await response.json(); 120 | const isError = !!error; 121 | const text = status ?? error; 122 | return { 123 | content: [{ type: "text", text: text }], 124 | isError, 125 | }; 126 | } catch (error: any) { 127 | return { 128 | content: [{ 129 | type: "text", 130 | text: error instanceof Error ? error.message : "Unknown error", 131 | }], 132 | isError: true, 133 | }; 134 | } 135 | } 136 | 137 | server.setRequestHandler(CallToolRequestSchema, async (request) => 138 | handleToolCall(request.params.name, request.params.arguments ?? {}) 139 | ); 140 | 141 | async function runServer() { 142 | const transport = new StdioServerTransport(); 143 | await server.connect(transport); 144 | const checkEndpoint = () => findWorkingIDEEndpoint().catch(); 145 | // We need to recheck the IDE endpoint every 10 seconds since IDE might be closed or restarted 146 | setInterval(checkEndpoint, 10000); 147 | await checkEndpoint(); 148 | console.error("JetBrains Proxy MCP Server running on stdio"); 149 | } 150 | 151 | runServer().catch(console.error); -------------------------------------------------------------------------------- /samples/node-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "resolveJsonModule": true, 11 | "noImplicitAny": false 12 | }, 13 | "include": [ 14 | "src/**/*" 15 | ], 16 | "exclude": [ 17 | "node_modules" 18 | ] 19 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" 3 | } 4 | rootProject.name = "kotlinx-mcp-sdk" 5 | 6 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/kotlinx/mcp/client/SSEClientTransport.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.kotlinx.mcp.client 2 | 3 | import org.jetbrains.kotlinx.mcp.JSONRPCMessage 4 | import io.ktor.client.* 5 | import io.ktor.client.plugins.sse.* 6 | import io.ktor.client.request.* 7 | import io.ktor.client.statement.* 8 | import io.ktor.http.* 9 | import kotlinx.coroutines.* 10 | import kotlinx.serialization.encodeToString 11 | import org.jetbrains.kotlinx.mcp.shared.McpJson 12 | import org.jetbrains.kotlinx.mcp.shared.Transport 13 | import java.util.concurrent.atomic.AtomicBoolean 14 | import kotlin.properties.Delegates 15 | import kotlin.time.Duration 16 | 17 | /** 18 | * Client transport for SSE: this will connect to a server using Server-Sent Events for receiving 19 | * messages and make separate POST requests for sending messages. 20 | */ 21 | public class SSEClientTransport( 22 | private val client: HttpClient, 23 | private val urlString: String?, 24 | private val reconnectionTime: Duration? = null, 25 | private val requestBuilder: HttpRequestBuilder.() -> Unit = {}, 26 | ) : Transport { 27 | private val scope by lazy { 28 | CoroutineScope(session.coroutineContext + SupervisorJob()) 29 | } 30 | 31 | private val initialized = AtomicBoolean(false) 32 | private var session: ClientSSESession by Delegates.notNull() 33 | private val endpoint = CompletableDeferred() 34 | 35 | override var onClose: (() -> Unit)? = null 36 | override var onError: ((Throwable) -> Unit)? = null 37 | override var onMessage: (suspend ((JSONRPCMessage) -> Unit))? = null 38 | 39 | private var job: Job? = null 40 | 41 | private val baseUrl by lazy { 42 | session.call.request.url.toString().removeSuffix("/") 43 | } 44 | 45 | override suspend fun start() { 46 | if (!initialized.compareAndSet(false, true)) { 47 | error( 48 | "SSEClientTransport already started! " + 49 | "If using Client class, note that connect() calls start() automatically.", 50 | ) 51 | } 52 | 53 | session = urlString?.let { 54 | client.sseSession( 55 | urlString = it, 56 | reconnectionTime = reconnectionTime, 57 | block = requestBuilder, 58 | ) 59 | } ?: client.sseSession( 60 | reconnectionTime = reconnectionTime, 61 | block = requestBuilder, 62 | ) 63 | 64 | job = scope.launch(CoroutineName("SseMcpClientTransport.collect#${hashCode()}")) { 65 | session.incoming.collect { event -> 66 | when (event.event) { 67 | "error" -> { 68 | val e = IllegalStateException("SSE error: ${event.data}") 69 | onError?.invoke(e) 70 | throw e 71 | } 72 | 73 | "open" -> { 74 | // The connection is open, but we need to wait for the endpoint to be received. 75 | } 76 | 77 | "endpoint" -> { 78 | try { 79 | val eventData = event.data ?: "" 80 | 81 | // check url correctness 82 | val maybeEndpoint = Url("$baseUrl/$eventData") 83 | 84 | endpoint.complete(maybeEndpoint.toString()) 85 | } catch (e: Exception) { 86 | onError?.invoke(e) 87 | close() 88 | error(e) 89 | } 90 | } 91 | 92 | else -> { 93 | try { 94 | val message = McpJson.decodeFromString(event.data ?: "") 95 | onMessage?.invoke(message) 96 | } catch (e: Exception) { 97 | onError?.invoke(e) 98 | } 99 | } 100 | } 101 | } 102 | } 103 | 104 | endpoint.await() 105 | } 106 | 107 | @OptIn(ExperimentalCoroutinesApi::class) 108 | override suspend fun send(message: JSONRPCMessage) { 109 | if (!endpoint.isCompleted) { 110 | error("Not connected") 111 | } 112 | 113 | try { 114 | val response = client.post(endpoint.getCompleted()) { 115 | headers.append(HttpHeaders.ContentType, ContentType.Application.Json) 116 | setBody(McpJson.encodeToString(message)) 117 | } 118 | 119 | if (!response.status.isSuccess()) { 120 | val text = response.bodyAsText() 121 | error("Error POSTing to endpoint (HTTP ${response.status}): $text") 122 | } 123 | } catch (e: Exception) { 124 | onError?.invoke(e) 125 | throw e 126 | } 127 | } 128 | 129 | override suspend fun close() { 130 | if (!initialized.get()) { 131 | error("SSEClientTransport is not initialized!") 132 | } 133 | 134 | session.cancel() 135 | onClose?.invoke() 136 | job?.cancelAndJoin() 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/kotlinx/mcp/client/StdioClientTransport.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.kotlinx.mcp.client 2 | 3 | import io.github.oshai.kotlinlogging.KotlinLogging 4 | import org.jetbrains.kotlinx.mcp.JSONRPCMessage 5 | import kotlinx.coroutines.* 6 | import kotlinx.coroutines.channels.Channel 7 | import kotlinx.coroutines.channels.consumeEach 8 | import org.jetbrains.kotlinx.mcp.shared.ReadBuffer 9 | import org.jetbrains.kotlinx.mcp.shared.Transport 10 | import org.jetbrains.kotlinx.mcp.shared.serializeMessage 11 | import java.io.InputStream 12 | import java.io.OutputStream 13 | import java.util.concurrent.atomic.AtomicBoolean 14 | import kotlin.coroutines.CoroutineContext 15 | import kotlin.text.Charsets.UTF_8 16 | 17 | /** 18 | * A transport implementation for JSON-RPC communication that leverages standard input and output streams. 19 | * 20 | * This class reads from an input stream to process incoming JSON-RPC messages and writes JSON-RPC messages 21 | * to an output stream. 22 | * 23 | * @param input The input stream where messages are received. 24 | * @param output The output stream where messages are sent. 25 | */ 26 | public class StdioClientTransport( 27 | private val input: InputStream, 28 | private val output: OutputStream 29 | ) : Transport { 30 | private val logger = KotlinLogging.logger {} 31 | private val ioCoroutineContext: CoroutineContext = Dispatchers.IO 32 | private val scope by lazy { 33 | CoroutineScope(ioCoroutineContext + SupervisorJob()) 34 | } 35 | private var job: Job? = null 36 | private var initialized = AtomicBoolean(false) 37 | private val sendChannel = Channel(Channel.UNLIMITED) 38 | private val readBuffer = ReadBuffer() 39 | 40 | override var onClose: (() -> Unit)? = null 41 | override var onError: ((Throwable) -> Unit)? = null 42 | override var onMessage: (suspend ((JSONRPCMessage) -> Unit))? = null 43 | 44 | override suspend fun start() { 45 | if (!initialized.compareAndSet(false, true)) { 46 | error("StdioClientTransport already started!") 47 | } 48 | 49 | logger.debug { "Starting StdioClientTransport..." } 50 | 51 | val outputStream = output.bufferedWriter(UTF_8) 52 | 53 | job = scope.launch(CoroutineName("StdioClientTransport.IO#${hashCode()}")) { 54 | val readJob = launch { 55 | logger.debug { "Read coroutine started." } 56 | try { 57 | val buffer = ByteArray(8192) 58 | while (isActive) { 59 | val bytesRead = input.read(buffer) 60 | if (bytesRead == -1) break 61 | if (bytesRead > 0) { 62 | readBuffer.append(buffer.copyOf(bytesRead)) 63 | processReadBuffer() 64 | } 65 | } 66 | } catch (e: CancellationException) { 67 | throw e 68 | } catch (e: Exception) { 69 | onError?.invoke(e) 70 | logger.error(e) { "Error reading from input stream" } 71 | } finally { 72 | input.close() 73 | } 74 | } 75 | 76 | val writeJob = launch { 77 | logger.debug { "Write coroutine started." } 78 | try { 79 | sendChannel.consumeEach { message -> 80 | val json = serializeMessage(message) 81 | outputStream.write(json) 82 | outputStream.flush() 83 | } 84 | } catch (e: Throwable) { 85 | if (isActive) { 86 | onError?.invoke(e) 87 | logger.error(e) { "Error writing to output stream" } 88 | } 89 | } finally { 90 | output.close() 91 | } 92 | } 93 | 94 | readJob.join() 95 | writeJob.cancelAndJoin() 96 | onClose?.invoke() 97 | } 98 | } 99 | 100 | override suspend fun send(message: JSONRPCMessage) { 101 | if (!initialized.get()) { 102 | error("Transport not started") 103 | } 104 | 105 | sendChannel.send(message) 106 | } 107 | 108 | override suspend fun close() { 109 | if (!initialized.compareAndSet(true, false)) { 110 | error("Transport is already closed") 111 | } 112 | job?.cancelAndJoin() 113 | input.close() 114 | output.close() 115 | readBuffer.clear() 116 | sendChannel.close() 117 | onClose?.invoke() 118 | } 119 | 120 | private suspend fun processReadBuffer() { 121 | while (true) { 122 | val msg = readBuffer.readMessage() ?: break 123 | try { 124 | onMessage?.invoke(msg) 125 | } catch (e: Throwable) { 126 | onError?.invoke(e) 127 | logger.error(e) { "Error processing message." } 128 | } 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/kotlinx/mcp/client/WebSocketClientTransport.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.kotlinx.mcp.client 2 | 3 | import io.ktor.client.* 4 | import io.ktor.client.plugins.websocket.* 5 | import io.ktor.client.request.* 6 | import io.ktor.http.* 7 | import io.ktor.websocket.* 8 | import org.jetbrains.kotlinx.mcp.shared.MCP_SUBPROTOCOL 9 | import org.jetbrains.kotlinx.mcp.shared.WebSocketMcpTransport 10 | import kotlin.properties.Delegates 11 | 12 | /** 13 | * Client transport for WebSocket: this will connect to a server over the WebSocket protocol. 14 | */ 15 | public class WebSocketClientTransport( 16 | private val client: HttpClient, 17 | private val urlString: String?, 18 | private val requestBuilder: HttpRequestBuilder.() -> Unit = {}, 19 | ) : WebSocketMcpTransport() { 20 | override var session: WebSocketSession by Delegates.notNull() 21 | 22 | override suspend fun initializeSession() { 23 | session = urlString?.let { 24 | client.webSocketSession(it) { 25 | requestBuilder() 26 | 27 | header(HttpHeaders.SecWebSocketProtocol, MCP_SUBPROTOCOL) 28 | } 29 | } ?: client.webSocketSession { 30 | requestBuilder() 31 | 32 | header(HttpHeaders.SecWebSocketProtocol, MCP_SUBPROTOCOL) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/kotlinx/mcp/client/WebSocketMcpKtorClientExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.kotlinx.mcp.client 2 | 3 | import org.jetbrains.kotlinx.mcp.Implementation 4 | import io.ktor.client.HttpClient 5 | import io.ktor.client.request.HttpRequestBuilder 6 | import org.jetbrains.kotlinx.mcp.LIB_VERSION 7 | import org.jetbrains.kotlinx.mcp.shared.IMPLEMENTATION_NAME 8 | 9 | /** 10 | * Returns a new WebSocket transport for the Model Context Protocol using the provided HttpClient. 11 | * 12 | * @param urlString Optional URL of the MCP server. 13 | * @param requestBuilder Optional lambda to configure the HTTP request. 14 | * @return A [WebSocketClientTransport] configured for MCP communication. 15 | */ 16 | public fun HttpClient.mcpWebSocketTransport( 17 | urlString: String? = null, 18 | requestBuilder: HttpRequestBuilder.() -> Unit = {}, 19 | ): WebSocketClientTransport = WebSocketClientTransport(this, urlString, requestBuilder) 20 | 21 | /** 22 | * Creates and connects an MCP client over WebSocket using the provided HttpClient. 23 | * 24 | * @param urlString Optional URL of the MCP server. 25 | * @param requestBuilder Optional lambda to configure the HTTP request. 26 | * @return A connected [Client] ready for MCP communication. 27 | */ 28 | public suspend fun HttpClient.mcpWebSocket( 29 | urlString: String? = null, 30 | requestBuilder: HttpRequestBuilder.() -> Unit = {}, 31 | ): Client { 32 | val transport = mcpWebSocketTransport(urlString, requestBuilder) 33 | val client = Client( 34 | Implementation( 35 | name = IMPLEMENTATION_NAME, 36 | version = LIB_VERSION, 37 | ) 38 | ) 39 | client.connect(transport) 40 | return client 41 | } 42 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/kotlinx/mcp/client/sse.ktor.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.kotlinx.mcp.client 2 | 3 | import org.jetbrains.kotlinx.mcp.Implementation 4 | import io.ktor.client.HttpClient 5 | import io.ktor.client.request.HttpRequestBuilder 6 | import org.jetbrains.kotlinx.mcp.LIB_VERSION 7 | import org.jetbrains.kotlinx.mcp.shared.IMPLEMENTATION_NAME 8 | import kotlin.time.Duration 9 | 10 | /** 11 | * Returns a new SSE transport for the Model Context Protocol using the provided HttpClient. 12 | * 13 | * @param urlString Optional URL of the MCP server. 14 | * @param reconnectionTime Optional duration to wait before attempting to reconnect. 15 | * @param requestBuilder Optional lambda to configure the HTTP request. 16 | * @return A [SSEClientTransport] configured for MCP communication. 17 | */ 18 | public fun HttpClient.mcpSseTransport( 19 | urlString: String? = null, 20 | reconnectionTime: Duration? = null, 21 | requestBuilder: HttpRequestBuilder.() -> Unit = {}, 22 | ): SSEClientTransport = SSEClientTransport(this, urlString, reconnectionTime, requestBuilder) 23 | 24 | /** 25 | * Creates and connects an MCP client over SSE using the provided HttpClient. 26 | * 27 | * @param urlString Optional URL of the MCP server. 28 | * @param reconnectionTime Optional duration to wait before attempting to reconnect. 29 | * @param requestBuilder Optional lambda to configure the HTTP request. 30 | * @return A connected [Client] ready for MCP communication. 31 | */ 32 | public suspend fun HttpClient.mcpSse( 33 | urlString: String? = null, 34 | reconnectionTime: Duration? = null, 35 | requestBuilder: HttpRequestBuilder.() -> Unit = {}, 36 | ): Client { 37 | val transport = mcpSseTransport(urlString, reconnectionTime, requestBuilder) 38 | val client = Client( 39 | Implementation( 40 | name = IMPLEMENTATION_NAME, 41 | version = LIB_VERSION, 42 | ) 43 | ) 44 | client.connect(transport) 45 | return client 46 | } 47 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/kotlinx/mcp/server/McpKtorServerPlugin.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.kotlinx.mcp.server 2 | 3 | import io.github.oshai.kotlinlogging.KotlinLogging 4 | import io.ktor.http.* 5 | import io.ktor.server.application.* 6 | import io.ktor.server.response.* 7 | import io.ktor.server.routing.* 8 | import io.ktor.server.sse.* 9 | import io.ktor.util.collections.* 10 | 11 | private val logger = KotlinLogging.logger {} 12 | 13 | /** 14 | * Configures the Ktor Application to handle Model Context Protocol (MCP) over Server-Sent Events (SSE). 15 | */ 16 | public fun Application.MCP(block: () -> Server) { 17 | val servers = ConcurrentMap() 18 | 19 | install(SSE) 20 | routing { 21 | sse("/sse") { 22 | val transport = SSEServerTransport("/message", this) 23 | logger.info { "New SSE connection established with sessionId: ${transport.sessionId}" } 24 | 25 | val server = block() 26 | 27 | servers[transport.sessionId] = server 28 | logger.debug { "Server instance created and stored for sessionId: ${transport.sessionId}" } 29 | 30 | server.onCloseCallback = { 31 | logger.info { "Server connection closed for sessionId: ${transport.sessionId}" } 32 | servers.remove(transport.sessionId) 33 | } 34 | 35 | server.connect(transport) 36 | logger.debug { "Server connected to transport for sessionId: ${transport.sessionId}" } 37 | } 38 | 39 | post("/message") { 40 | val sessionId: String = call.request.queryParameters["sessionId"]!! 41 | logger.debug { "Received message for sessionId: $sessionId" } 42 | 43 | val transport = servers[sessionId]?.transport as? SSEServerTransport 44 | if (transport == null) { 45 | logger.warn { "Session not found for sessionId: $sessionId" } 46 | call.respond(HttpStatusCode.NotFound, "Session not found") 47 | return@post 48 | } 49 | 50 | transport.handlePostMessage(call) 51 | logger.trace { "Message handled for sessionId: $sessionId" } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/kotlinx/mcp/server/SSEServerTransport.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.kotlinx.mcp.server 2 | 3 | import org.jetbrains.kotlinx.mcp.JSONRPCMessage 4 | import io.ktor.http.* 5 | import io.ktor.server.application.* 6 | import io.ktor.server.request.* 7 | import io.ktor.server.response.* 8 | import io.ktor.server.sse.* 9 | import kotlinx.coroutines.job 10 | import kotlinx.serialization.encodeToString 11 | import org.jetbrains.kotlinx.mcp.shared.McpJson 12 | import org.jetbrains.kotlinx.mcp.shared.Transport 13 | import java.util.concurrent.atomic.AtomicBoolean 14 | import kotlin.uuid.ExperimentalUuidApi 15 | import kotlin.uuid.Uuid 16 | 17 | internal const val SESSION_ID_PARAM = "sessionId" 18 | 19 | /** 20 | * Server transport for SSE: this will send messages over an SSE connection and receive messages from HTTP POST requests. 21 | * 22 | * Creates a new SSE server transport, which will direct the client to POST messages to the relative or absolute URL identified by `_endpoint`. 23 | */ 24 | public class SSEServerTransport( 25 | private val endpoint: String, 26 | private val session: ServerSSESession, 27 | ) : Transport { 28 | private val initialized = AtomicBoolean(false) 29 | 30 | @OptIn(ExperimentalUuidApi::class) 31 | public val sessionId: String = Uuid.random().toString() 32 | 33 | override var onClose: (() -> Unit)? = null 34 | override var onError: ((Throwable) -> Unit)? = null 35 | override var onMessage: (suspend ((JSONRPCMessage) -> Unit))? = null 36 | 37 | /** 38 | * Handles the initial SSE connection request. 39 | * 40 | * This should be called when a GET request is made to establish the SSE stream. 41 | */ 42 | override suspend fun start() { 43 | if (!initialized.compareAndSet(false, true)) { 44 | throw error("SSEServerTransport already started! If using Server class, note that connect() calls start() automatically.") 45 | } 46 | 47 | // Send the endpoint event 48 | session.send( 49 | event = "endpoint", 50 | data = "${endpoint.encodeURLPath()}?$SESSION_ID_PARAM=${sessionId}", 51 | ) 52 | 53 | try { 54 | session.coroutineContext.job.join() 55 | } finally { 56 | onClose?.invoke() 57 | } 58 | } 59 | 60 | /** 61 | * Handles incoming POST messages. 62 | * 63 | * This should be called when a POST request is made to send a message to the server. 64 | */ 65 | public suspend fun handlePostMessage(call: ApplicationCall) { 66 | if (!initialized.get()) { 67 | val message = "SSE connection not established" 68 | call.respondText(message, status = HttpStatusCode.InternalServerError) 69 | onError?.invoke(IllegalStateException(message)) 70 | } 71 | 72 | val body = try { 73 | val ct = call.request.contentType() 74 | if (ct != ContentType.Application.Json) { 75 | error("Unsupported content-type: $ct") 76 | } 77 | 78 | call.receiveText() 79 | } catch (e: Exception) { 80 | call.respondText("Invalid message: ${e.message}", status = HttpStatusCode.BadRequest) 81 | onError?.invoke(e) 82 | return 83 | } 84 | 85 | try { 86 | handleMessage(body) 87 | } catch (e: Exception) { 88 | call.respondText("Error handling message $body: ${e.message}", status = HttpStatusCode.BadRequest) 89 | return 90 | } 91 | 92 | call.respondText("Accepted", status = HttpStatusCode.Accepted) 93 | } 94 | 95 | /** 96 | * Handle a client message, regardless of how it arrived. 97 | * This can be used to inform the server of messages that arrive via a means different from HTTP POST. 98 | */ 99 | public suspend fun handleMessage(message: String) { 100 | try { 101 | val parsedMessage = McpJson.decodeFromString(message) 102 | onMessage?.invoke(parsedMessage) 103 | } catch (e: Exception) { 104 | onError?.invoke(e) 105 | throw e 106 | } 107 | } 108 | 109 | override suspend fun close() { 110 | session.close() 111 | onClose?.invoke() 112 | } 113 | 114 | override suspend fun send(message: JSONRPCMessage) { 115 | if (!initialized.get()) { 116 | throw error("Not connected") 117 | } 118 | 119 | session.send( 120 | event = "message", 121 | data = McpJson.encodeToString(message), 122 | ) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/kotlinx/mcp/server/StdioServerTransport.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.kotlinx.mcp.server 2 | 3 | import io.github.oshai.kotlinlogging.KotlinLogging 4 | import org.jetbrains.kotlinx.mcp.JSONRPCMessage 5 | import kotlinx.coroutines.* 6 | import kotlinx.coroutines.channels.Channel 7 | import org.jetbrains.kotlinx.mcp.shared.ReadBuffer 8 | import org.jetbrains.kotlinx.mcp.shared.Transport 9 | import org.jetbrains.kotlinx.mcp.shared.serializeMessage 10 | import java.io.BufferedInputStream 11 | import java.io.PrintStream 12 | import java.util.concurrent.atomic.AtomicBoolean 13 | import kotlin.coroutines.CoroutineContext 14 | 15 | /** 16 | * A server transport that communicates with a client via standard I/O. 17 | * 18 | * Reads from System.in and writes to System.out. 19 | */ 20 | public class StdioServerTransport( 21 | private val inputStream: BufferedInputStream = BufferedInputStream(System.`in`), 22 | outputStream: PrintStream = System.out 23 | ) : Transport { 24 | private val logger = KotlinLogging.logger {} 25 | override var onClose: (() -> Unit)? = null 26 | override var onError: ((Throwable) -> Unit)? = null 27 | override var onMessage: (suspend (JSONRPCMessage) -> Unit)? = null 28 | 29 | private val readBuffer = ReadBuffer() 30 | private var initialized = AtomicBoolean(false) 31 | private var readingJob: Job? = null 32 | 33 | private val coroutineContext: CoroutineContext = Dispatchers.IO + SupervisorJob() 34 | private val scope = CoroutineScope(coroutineContext) 35 | private val readChannel = Channel(Channel.UNLIMITED) 36 | private val outputWriter = outputStream.bufferedWriter() 37 | 38 | override suspend fun start() { 39 | if (!initialized.compareAndSet(false, true)) { 40 | error("StdioServerTransport already started!") 41 | } 42 | 43 | // Launch a coroutine to read from stdin 44 | readingJob = scope.launch { 45 | val buf = ByteArray(8192) 46 | try { 47 | while (isActive) { 48 | val bytesRead = inputStream.read(buf) 49 | if (bytesRead == -1) { 50 | // EOF reached 51 | break 52 | } 53 | if (bytesRead > 0) { 54 | val chunk = buf.copyOf(bytesRead) 55 | readChannel.send(chunk) 56 | } 57 | } 58 | } catch (e: Throwable) { 59 | logger.error(e) { "Error reading from stdin" } 60 | onError?.invoke(e) 61 | } finally { 62 | // Reached EOF or error, close connection 63 | close() 64 | } 65 | } 66 | 67 | // Launch a coroutine to process messages from readChannel 68 | scope.launch { 69 | try { 70 | for (chunk in readChannel) { 71 | readBuffer.append(chunk) 72 | processReadBuffer() 73 | } 74 | } catch (e: Throwable) { 75 | onError?.invoke(e) 76 | } 77 | } 78 | } 79 | 80 | private suspend fun processReadBuffer() { 81 | while (true) { 82 | val message = try { 83 | readBuffer.readMessage() 84 | } catch (e: Throwable) { 85 | onError?.invoke(e) 86 | null 87 | } 88 | 89 | if (message == null) break 90 | // Async invocation broke delivery order 91 | try { 92 | onMessage?.invoke(message) 93 | } catch (e: Throwable) { 94 | onError?.invoke(e) 95 | } 96 | } 97 | } 98 | 99 | override suspend fun close() { 100 | if (!initialized.compareAndSet(true, false)) return 101 | 102 | // Cancel reading job and close channel 103 | readingJob?.cancel() // ToDO("was cancel and join") 104 | readChannel.close() 105 | readBuffer.clear() 106 | 107 | onClose?.invoke() 108 | } 109 | 110 | override suspend fun send(message: JSONRPCMessage) { 111 | val json = serializeMessage(message) 112 | synchronized(outputWriter) { 113 | // You may need to add Content-Length headers before the message if using the LSP framing protocol 114 | outputWriter.write(json) 115 | outputWriter.flush() 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/kotlinx/mcp/server/WebSocketMcpKtorServerExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.kotlinx.mcp.server 2 | 3 | import org.jetbrains.kotlinx.mcp.Implementation 4 | import org.jetbrains.kotlinx.mcp.ServerCapabilities 5 | import io.ktor.server.routing.* 6 | import io.ktor.server.websocket.* 7 | import org.jetbrains.kotlinx.mcp.LIB_VERSION 8 | import org.jetbrains.kotlinx.mcp.shared.IMPLEMENTATION_NAME 9 | 10 | /** 11 | * Registers a WebSocket route that establishes an MCP (Model Context Protocol) server session. 12 | * 13 | * @param options Optional server configuration settings for the MCP server. 14 | * @param handler A suspend function that defines the server's behavior. 15 | */ 16 | public fun Route.mcpWebSocket( 17 | options: ServerOptions? = null, 18 | handler: suspend Server.() -> Unit = {}, 19 | ) { 20 | webSocket { 21 | createMcpServer(this, options, handler) 22 | } 23 | } 24 | 25 | /** 26 | * Registers a WebSocket route at the specified [path] that establishes an MCP server session. 27 | * 28 | * @param path The URL path at which to register the WebSocket route. 29 | * @param options Optional server configuration settings for the MCP server. 30 | * @param handler A suspend function that defines the server's behavior. 31 | */ 32 | public fun Route.mcpWebSocket( 33 | path: String, 34 | options: ServerOptions? = null, 35 | handler: suspend Server.() -> Unit = {}, 36 | ) { 37 | webSocket(path) { 38 | createMcpServer(this, options, handler) 39 | } 40 | } 41 | 42 | /** 43 | * Registers a WebSocket route that creates an MCP server transport layer. 44 | * 45 | * @param handler A suspend function that defines the behavior of the transport layer. 46 | */ 47 | public fun Route.mcpWebSocketTransport( 48 | handler: suspend WebSocketMcpServerTransport.() -> Unit = {}, 49 | ) { 50 | webSocket { 51 | val transport = createMcpTransport(this) 52 | transport.start() 53 | handler(transport) 54 | transport.close() 55 | } 56 | } 57 | 58 | /** 59 | * Registers a WebSocket route at the specified [path] that creates an MCP server transport layer. 60 | * 61 | * @param path The URL path at which to register the WebSocket route. 62 | * @param handler A suspend function that defines the behavior of the transport layer. 63 | */ 64 | public fun Route.mcpWebSocketTransport( 65 | path: String, 66 | handler: suspend WebSocketMcpServerTransport.() -> Unit = {}, 67 | ) { 68 | webSocket(path) { 69 | val transport = createMcpTransport(this) 70 | transport.start() 71 | handler(transport) 72 | transport.close() 73 | } 74 | } 75 | 76 | 77 | private suspend fun Route.createMcpServer( 78 | session: WebSocketServerSession, 79 | options: ServerOptions?, 80 | handler: suspend Server.() -> Unit, 81 | ) { 82 | val transport = createMcpTransport(session) 83 | 84 | val server = Server( 85 | serverInfo = Implementation( 86 | name = IMPLEMENTATION_NAME, 87 | version = LIB_VERSION, 88 | ), 89 | options = options ?: ServerOptions( 90 | capabilities = ServerCapabilities( 91 | prompts = ServerCapabilities.Prompts(listChanged = null), 92 | resources = ServerCapabilities.Resources(subscribe = null, listChanged = null), 93 | tools = ServerCapabilities.Tools(listChanged = null), 94 | ) 95 | ), 96 | ) 97 | 98 | server.connect(transport) 99 | handler(server) 100 | server.close() 101 | } 102 | 103 | private fun createMcpTransport( 104 | session: WebSocketServerSession, 105 | ): WebSocketMcpServerTransport { 106 | return WebSocketMcpServerTransport(session) 107 | } 108 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/kotlinx/mcp/server/WebSocketMcpServerTransport.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.kotlinx.mcp.server 2 | 3 | import io.ktor.http.* 4 | import io.ktor.server.websocket.* 5 | import org.jetbrains.kotlinx.mcp.shared.MCP_SUBPROTOCOL 6 | import org.jetbrains.kotlinx.mcp.shared.WebSocketMcpTransport 7 | 8 | /** 9 | * Server-side implementation of the MCP (Model Context Protocol) transport over WebSocket. 10 | * 11 | * @property session The WebSocket server session used for communication. 12 | */ 13 | public class WebSocketMcpServerTransport( 14 | override val session: WebSocketServerSession, 15 | ) : WebSocketMcpTransport() { 16 | override suspend fun initializeSession() { 17 | val subprotocol = session.call.request.headers[HttpHeaders.SecWebSocketProtocol] 18 | if (subprotocol != MCP_SUBPROTOCOL) { 19 | error("Invalid subprotocol: $subprotocol, expected $MCP_SUBPROTOCOL") 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/kotlinx/mcp/shared/ReadBuffer.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.kotlinx.mcp.shared 2 | 3 | import org.jetbrains.kotlinx.mcp.JSONRPCMessage 4 | import io.ktor.utils.io.core.* 5 | import kotlinx.io.Buffer 6 | import kotlinx.io.indexOf 7 | import kotlinx.io.readString 8 | import kotlinx.serialization.encodeToString 9 | 10 | /** 11 | * Buffers a continuous stdio stream into discrete JSON-RPC messages. 12 | */ 13 | public class ReadBuffer { 14 | private val buffer: Buffer = Buffer() 15 | 16 | public fun append(chunk: ByteArray) { 17 | buffer.writeFully(chunk) 18 | } 19 | 20 | public fun readMessage(): JSONRPCMessage? { 21 | if (buffer.exhausted()) return null 22 | var lfIndex = buffer.indexOf('\n'.code.toByte()) 23 | val line = when (lfIndex) { 24 | -1L -> return null 25 | 0L -> { 26 | buffer.skip(1) 27 | "" 28 | } 29 | 30 | else -> { 31 | var skipBytes = 1 32 | if (buffer[lfIndex - 1] == '\r'.code.toByte()) { 33 | lfIndex -= 1 34 | skipBytes += 1 35 | } 36 | val string = buffer.readString(lfIndex) 37 | buffer.skip(skipBytes.toLong()) 38 | string 39 | } 40 | } 41 | return deserializeMessage(line) 42 | } 43 | 44 | public fun clear() { 45 | buffer.clear() 46 | } 47 | } 48 | 49 | internal fun deserializeMessage(line: String): JSONRPCMessage { 50 | return McpJson.decodeFromString(line) 51 | } 52 | 53 | internal fun serializeMessage(message: JSONRPCMessage): String { 54 | return McpJson.encodeToString(message) + "\n" 55 | } 56 | 57 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/kotlinx/mcp/shared/Transport.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.kotlinx.mcp.shared 2 | 3 | import org.jetbrains.kotlinx.mcp.JSONRPCMessage 4 | 5 | /** 6 | * Describes the minimal contract for a MCP transport that a client or server can communicate over. 7 | */ 8 | public interface Transport { 9 | /** 10 | * Starts processing messages on the transport, including any connection steps that might need to be taken. 11 | * 12 | * This method should only be called after callbacks are installed, or else messages may be lost. 13 | * 14 | * NOTE: This method should not be called explicitly when using Client, Server, or Protocol classes, 15 | * as they will implicitly call start(). 16 | */ 17 | public suspend fun start() 18 | 19 | /** 20 | * Sends a JSON-RPC message (request or response). 21 | */ 22 | public suspend fun send(message: JSONRPCMessage) 23 | 24 | /** 25 | * Closes the connection. 26 | */ 27 | public suspend fun close() 28 | 29 | /** 30 | * Callback for when the connection is closed for any reason. 31 | * 32 | * This should be invoked when close() is called as well. 33 | */ 34 | public var onClose: (() -> Unit)? 35 | 36 | /** 37 | * Callback for when an error occurs. 38 | * 39 | * Note that errors are not necessarily fatal; they are used for reporting any kind of 40 | * exceptional condition out of band. 41 | */ 42 | public var onError: ((Throwable) -> Unit)? 43 | 44 | /** 45 | * Callback for when a message (request or response) is received over the connection. 46 | */ 47 | public var onMessage: (suspend ((JSONRPCMessage) -> Unit))? 48 | } 49 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/kotlinx/mcp/shared/WebSocketMcpTransport.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.kotlinx.mcp.shared 2 | 3 | import org.jetbrains.kotlinx.mcp.JSONRPCMessage 4 | import io.ktor.websocket.Frame 5 | import io.ktor.websocket.WebSocketSession 6 | import io.ktor.websocket.close 7 | import io.ktor.websocket.readText 8 | import kotlinx.coroutines.CoroutineName 9 | import kotlinx.coroutines.CoroutineScope 10 | import kotlinx.coroutines.InternalCoroutinesApi 11 | import kotlinx.coroutines.SupervisorJob 12 | import kotlinx.coroutines.channels.ClosedReceiveChannelException 13 | import kotlinx.coroutines.job 14 | import kotlinx.coroutines.launch 15 | import kotlinx.serialization.encodeToString 16 | import java.util.concurrent.atomic.AtomicBoolean 17 | 18 | internal const val MCP_SUBPROTOCOL = "mcp" 19 | 20 | /** 21 | * Abstract class representing a WebSocket transport for the Model Context Protocol (MCP). 22 | * Handles communication over a WebSocket session. 23 | */ 24 | public abstract class WebSocketMcpTransport : Transport { 25 | private val scope by lazy { 26 | CoroutineScope(session.coroutineContext + SupervisorJob()) 27 | } 28 | 29 | private val initialized = AtomicBoolean(false) 30 | /** 31 | * The WebSocket session used for communication. 32 | */ 33 | protected abstract val session: WebSocketSession 34 | 35 | override var onClose: (() -> Unit)? = null 36 | override var onError: ((Throwable) -> Unit)? = null 37 | override var onMessage: (suspend ((JSONRPCMessage) -> Unit))? = null 38 | 39 | /** 40 | * Initializes the WebSocket session 41 | */ 42 | protected abstract suspend fun initializeSession() 43 | 44 | override suspend fun start() { 45 | if (!initialized.compareAndSet(false, true)) { 46 | error( 47 | "WebSocketClientTransport already started! " + 48 | "If using Client class, note that connect() calls start() automatically.", 49 | ) 50 | } 51 | 52 | initializeSession() 53 | 54 | scope.launch(CoroutineName("WebSocketMcpTransport.collect#${hashCode()}")) { 55 | while (true) { 56 | val message = try { 57 | session.incoming.receive() 58 | } catch (_: ClosedReceiveChannelException) { 59 | return@launch 60 | } 61 | 62 | if (message !is Frame.Text) { 63 | val e = IllegalArgumentException("Expected text frame, got ${message::class.simpleName}: $message") 64 | onError?.invoke(e) 65 | throw e 66 | } 67 | 68 | try { 69 | val message = McpJson.decodeFromString(message.readText()) 70 | onMessage?.invoke(message) 71 | } catch (e: Exception) { 72 | onError?.invoke(e) 73 | throw e 74 | } 75 | } 76 | } 77 | 78 | @OptIn(InternalCoroutinesApi::class) 79 | session.coroutineContext.job.invokeOnCompletion { 80 | if (it != null) { 81 | onError?.invoke(it) 82 | } else { 83 | onClose?.invoke() 84 | } 85 | } 86 | } 87 | 88 | override suspend fun send(message: JSONRPCMessage) { 89 | if (!initialized.get()) { 90 | error("Not connected") 91 | } 92 | 93 | session.outgoing.send(Frame.Text(McpJson.encodeToString(message))) 94 | } 95 | 96 | override suspend fun close() { 97 | if (!initialized.get()) { 98 | error("Not connected") 99 | } 100 | 101 | session.close() 102 | session.coroutineContext.job.join() 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/test/kotlin/InMemoryTransport.kt: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlinx.mcp.JSONRPCMessage 2 | import org.jetbrains.kotlinx.mcp.shared.Transport 3 | 4 | /** 5 | * In-memory transport for creating clients and servers that talk to each other within the same process. 6 | */ 7 | class InMemoryTransport : Transport { 8 | private var otherTransport: InMemoryTransport? = null 9 | private val messageQueue: MutableList = mutableListOf() 10 | 11 | override var onClose: (() -> Unit)? = null 12 | override var onError: ((Throwable) -> Unit)? = null 13 | override var onMessage: (suspend ((JSONRPCMessage) -> Unit))? = null 14 | 15 | /** 16 | * Creates a pair of linked in-memory transports that can communicate with each other. 17 | * One should be passed to a Client and one to a Server. 18 | */ 19 | companion object { 20 | fun createLinkedPair(): Pair { 21 | val clientTransport = InMemoryTransport() 22 | val serverTransport = InMemoryTransport() 23 | clientTransport.otherTransport = serverTransport 24 | serverTransport.otherTransport = clientTransport 25 | return Pair(clientTransport, serverTransport) 26 | } 27 | } 28 | 29 | override suspend fun start() { 30 | // Process any messages that were queued before start was called 31 | while (messageQueue.isNotEmpty()) { 32 | messageQueue.removeFirstOrNull()?.let { message -> 33 | onMessage?.invoke(message) // todo? 34 | } 35 | } 36 | } 37 | 38 | override suspend fun close() { 39 | val other = otherTransport 40 | otherTransport = null 41 | other?.close() 42 | onClose?.invoke() 43 | } 44 | 45 | override suspend fun send(message: JSONRPCMessage) { 46 | val other = otherTransport ?: throw IllegalStateException("Not connected") 47 | 48 | if (other.onMessage != null) { 49 | other.onMessage?.invoke(message) // todo? 50 | } else { 51 | other.messageQueue.add(message) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/test/kotlin/client/BaseTransportTest.kt: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import org.jetbrains.kotlinx.mcp.InitializedNotification 4 | import org.jetbrains.kotlinx.mcp.JSONRPCMessage 5 | import org.jetbrains.kotlinx.mcp.PingRequest 6 | import kotlinx.coroutines.CompletableDeferred 7 | import org.jetbrains.kotlinx.mcp.shared.Transport 8 | import org.jetbrains.kotlinx.mcp.toJSON 9 | import kotlin.test.assertEquals 10 | import kotlin.test.assertFalse 11 | import kotlin.test.assertTrue 12 | import kotlin.test.fail 13 | 14 | abstract class BaseTransportTest { 15 | protected suspend fun testClientOpenClose(client: Transport) { 16 | client.onError = { error -> 17 | fail("Unexpected error: $error") 18 | } 19 | 20 | var didClose = false 21 | client.onClose = { didClose = true } 22 | 23 | client.start() 24 | assertFalse(didClose, "Transport should not be closed immediately after start") 25 | 26 | client.close() 27 | assertTrue(didClose, "Transport should be closed after close() call") 28 | } 29 | 30 | protected suspend fun testClientRead(client: Transport) { 31 | client.onError = { error -> 32 | error.printStackTrace() 33 | fail("Unexpected error: $error") 34 | } 35 | 36 | val messages = listOf( 37 | PingRequest().toJSON(), 38 | InitializedNotification().toJSON() 39 | ) 40 | 41 | val readMessages = mutableListOf() 42 | val finished = CompletableDeferred() 43 | 44 | client.onMessage = { message -> 45 | readMessages.add(message) 46 | if (message == messages.last()) { 47 | finished.complete(Unit) 48 | } 49 | } 50 | 51 | client.start() 52 | 53 | for (message in messages) { 54 | client.send(message) 55 | } 56 | 57 | finished.await() 58 | 59 | assertEquals(messages, readMessages, "Assert messages received") 60 | 61 | client.close() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/test/kotlin/client/ClientIntegrationTest.kt: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import org.jetbrains.kotlinx.mcp.Implementation 4 | import org.jetbrains.kotlinx.mcp.ListToolsResult 5 | import kotlinx.coroutines.test.runTest 6 | import org.jetbrains.kotlinx.mcp.client.Client 7 | import org.jetbrains.kotlinx.mcp.client.StdioClientTransport 8 | import org.junit.jupiter.api.Disabled 9 | import org.junit.jupiter.api.Test 10 | import java.net.Socket 11 | 12 | class ClientIntegrationTest { 13 | 14 | fun createTransport(): StdioClientTransport { 15 | val socket = Socket("localhost", 3000) 16 | 17 | return StdioClientTransport(socket.inputStream, socket.outputStream) 18 | } 19 | 20 | @Disabled("This test requires a running server") 21 | @Test 22 | fun testRequestTools() = runTest { 23 | val client = Client( 24 | Implementation("test", "1.0"), 25 | ) 26 | 27 | val transport = createTransport() 28 | try { 29 | client.connect(transport) 30 | 31 | val response: ListToolsResult? = client.listTools() 32 | println(response?.tools) 33 | 34 | } finally { 35 | transport.close() 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/test/kotlin/client/InMemoryTransportTest.kt: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import InMemoryTransport 4 | import org.jetbrains.kotlinx.mcp.InitializedNotification 5 | import org.jetbrains.kotlinx.mcp.JSONRPCMessage 6 | import kotlinx.coroutines.runBlocking 7 | import org.jetbrains.kotlinx.mcp.toJSON 8 | import org.junit.jupiter.api.BeforeEach 9 | import org.junit.jupiter.api.Test 10 | import org.junit.jupiter.api.assertThrows 11 | import kotlin.test.assertEquals 12 | import kotlin.test.assertNotNull 13 | import kotlin.test.assertTrue 14 | 15 | class InMemoryTransportTest { 16 | private lateinit var clientTransport: InMemoryTransport 17 | private lateinit var serverTransport: InMemoryTransport 18 | 19 | @BeforeEach 20 | fun setUp() { 21 | val (client, server) = InMemoryTransport.createLinkedPair() 22 | clientTransport = client 23 | serverTransport = server 24 | } 25 | 26 | @Test 27 | fun `should create linked pair`() { 28 | assertNotNull(clientTransport) 29 | assertNotNull(serverTransport) 30 | } 31 | 32 | @Test 33 | fun `should start without error`() { 34 | runBlocking { 35 | clientTransport.start() 36 | serverTransport.start() 37 | // If no exception is thrown, the test passes 38 | } 39 | } 40 | 41 | @Test 42 | fun `should send message from client to server`() { 43 | runBlocking { 44 | val message = InitializedNotification() 45 | 46 | var receivedMessage: JSONRPCMessage? = null 47 | serverTransport.onMessage = { msg -> 48 | receivedMessage = msg 49 | } 50 | 51 | val rpcNotification = message.toJSON() 52 | clientTransport.send(rpcNotification) 53 | assertEquals(rpcNotification, receivedMessage) 54 | } 55 | } 56 | 57 | @Test 58 | fun `should send message from server to client`() { 59 | runBlocking { 60 | val message = InitializedNotification() 61 | .toJSON() 62 | 63 | var receivedMessage: JSONRPCMessage? = null 64 | clientTransport.onMessage = { msg -> 65 | receivedMessage = msg 66 | } 67 | 68 | serverTransport.send(message) 69 | assertEquals(message, receivedMessage) 70 | } 71 | } 72 | 73 | @Test 74 | fun `should handle close`() { 75 | runBlocking { 76 | var clientClosed = false 77 | var serverClosed = false 78 | 79 | clientTransport.onClose = { 80 | clientClosed = true 81 | } 82 | 83 | serverTransport.onClose = { 84 | serverClosed = true 85 | } 86 | 87 | clientTransport.close() 88 | assertTrue(clientClosed) 89 | assertTrue(serverClosed) 90 | } 91 | } 92 | 93 | @Test 94 | fun `should throw error when sending after close`() { 95 | runBlocking { 96 | clientTransport.close() 97 | 98 | assertThrows { 99 | runBlocking { 100 | clientTransport.send( 101 | InitializedNotification().toJSON() 102 | ) 103 | } 104 | } 105 | } 106 | } 107 | 108 | @Test 109 | fun `should queue messages sent before start`() { 110 | runBlocking { 111 | val message = InitializedNotification() 112 | .toJSON() 113 | 114 | var receivedMessage: JSONRPCMessage? = null 115 | serverTransport.onMessage = { msg -> 116 | receivedMessage = msg 117 | } 118 | 119 | clientTransport.send(message) 120 | serverTransport.start() 121 | assertEquals(message, receivedMessage) 122 | } 123 | } 124 | } -------------------------------------------------------------------------------- /src/test/kotlin/client/SseTransportTest.kt: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import io.ktor.client.* 4 | import io.ktor.client.plugins.sse.* 5 | import io.ktor.server.application.* 6 | import io.ktor.server.cio.* 7 | import io.ktor.server.engine.* 8 | import io.ktor.server.routing.* 9 | import kotlinx.coroutines.CompletableDeferred 10 | import kotlinx.coroutines.test.runTest 11 | import mcpSse 12 | import mcpSseTransport 13 | import org.jetbrains.kotlinx.mcp.client.mcpSseTransport 14 | import org.junit.jupiter.api.Test 15 | 16 | private const val PORT = 8080 17 | 18 | class SseTransportTest : BaseTransportTest() { 19 | @Test 20 | fun `should start then close cleanly`() = runTest { 21 | val server = embeddedServer(CIO, port = PORT) { 22 | install(io.ktor.server.sse.SSE) 23 | routing { 24 | mcpSse() 25 | } 26 | }.start(wait = false) 27 | 28 | val client = HttpClient { 29 | install(SSE) 30 | }.mcpSseTransport { 31 | url { 32 | host = "localhost" 33 | port = PORT 34 | } 35 | } 36 | 37 | testClientOpenClose(client) 38 | 39 | server.stop() 40 | } 41 | 42 | @Test 43 | fun `should read messages`() = runTest { 44 | val clientFinished = CompletableDeferred() 45 | 46 | val server = embeddedServer(CIO, port = PORT) { 47 | install(io.ktor.server.sse.SSE) 48 | routing { 49 | mcpSseTransport { 50 | onMessage = { 51 | send(it) 52 | } 53 | } 54 | } 55 | }.start(wait = false) 56 | 57 | val client = HttpClient { 58 | install(SSE) 59 | }.mcpSseTransport { 60 | url { 61 | host = "localhost" 62 | port = PORT 63 | } 64 | } 65 | 66 | testClientRead(client) 67 | server.stop() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/test/kotlin/client/StdioClientTransportTest.kt: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import kotlinx.coroutines.test.runTest 4 | import org.jetbrains.kotlinx.mcp.client.StdioClientTransport 5 | import org.junit.jupiter.api.Test 6 | 7 | class StdioClientTransportTest : BaseTransportTest() { 8 | @Test 9 | fun `should start then close cleanly`() = runTest { 10 | // Run process "/usr/bin/tee" 11 | val processBuilder = ProcessBuilder("/usr/bin/tee") 12 | val process = processBuilder.start() 13 | 14 | val input = process.inputStream 15 | val output = process.outputStream 16 | 17 | val client = StdioClientTransport( 18 | input = input, 19 | output = output 20 | ) 21 | 22 | testClientOpenClose(client) 23 | 24 | process.destroy() 25 | } 26 | 27 | @Test 28 | fun `should read messages`() = runTest { 29 | val processBuilder = ProcessBuilder("/usr/bin/tee") 30 | val process = processBuilder.start() 31 | 32 | val input = process.inputStream 33 | val output = process.outputStream 34 | 35 | val client = StdioClientTransport( 36 | input = input, 37 | output = output 38 | ) 39 | 40 | testClientRead(client) 41 | 42 | process.waitFor() 43 | process.destroy() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/kotlin/client/TypesTest.kt: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import org.jetbrains.kotlinx.mcp.JSONRPCMessage 4 | import org.jetbrains.kotlinx.mcp.Request 5 | import org.junit.jupiter.api.Test 6 | import org.jetbrains.kotlinx.mcp.shared.McpJson 7 | 8 | class TypesTest { 9 | 10 | @Test 11 | fun testRequestResult() { 12 | val message = "{\"result\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{\"tools\":{\"listChanged\":true},\"resources\":{}},\"serverInfo\":{\"name\":\"jetbrains/proxy\",\"version\":\"0.1.0\"}},\"jsonrpc\":\"2.0\",\"id\":1}" 13 | McpJson.decodeFromString(message) 14 | } 15 | 16 | @Test 17 | fun testRequestError() { 18 | val message = "{\"method\":\"initialize\", \"protocolVersion\":\"2024-11-05\",\"capabilities\":{\"experimental\":{},\"sampling\":{}},\"clientInfo\":{\"name\":\"test client\",\"version\":\"1.0\"},\"_meta\":{}}" 19 | McpJson.decodeFromString(message) 20 | } 21 | 22 | @Test 23 | fun testJSONRPCMessage() { 24 | val line = "{\"result\":{\"content\":[{\"type\":\"text\"}],\"isError\":false},\"jsonrpc\":\"2.0\",\"id\":4}" 25 | McpJson.decodeFromString(line) 26 | } 27 | } -------------------------------------------------------------------------------- /src/test/kotlin/client/WebSocketTransportTest.kt: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import io.ktor.server.testing.* 4 | import io.ktor.server.websocket.* 5 | import kotlinx.coroutines.CompletableDeferred 6 | import org.jetbrains.kotlinx.mcp.client.mcpWebSocketTransport 7 | import org.junit.jupiter.api.Test 8 | import org.jetbrains.kotlinx.mcp.server.mcpWebSocket 9 | import org.jetbrains.kotlinx.mcp.server.mcpWebSocketTransport 10 | 11 | class WebSocketTransportTest : BaseTransportTest() { 12 | @Test 13 | fun `should start then close cleanly`() = testApplication { 14 | install(WebSockets) 15 | routing { 16 | mcpWebSocket() 17 | } 18 | 19 | val client = createClient { 20 | install(io.ktor.client.plugins.websocket.WebSockets) 21 | }.mcpWebSocketTransport() 22 | 23 | testClientOpenClose(client) 24 | } 25 | 26 | @Test 27 | fun `should read messages`() = testApplication { 28 | val clientFinished = CompletableDeferred() 29 | 30 | install(WebSockets) 31 | routing { 32 | mcpWebSocketTransport { 33 | onMessage = { 34 | send(it) 35 | } 36 | 37 | clientFinished.await() 38 | } 39 | } 40 | 41 | val client = createClient { 42 | install(io.ktor.client.plugins.websocket.WebSockets) 43 | }.mcpWebSocketTransport() 44 | 45 | testClientRead(client) 46 | 47 | clientFinished.complete(Unit) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/kotlin/server/StdioServerTransportTest.kt: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import org.jetbrains.kotlinx.mcp.InitializedNotification 4 | import org.jetbrains.kotlinx.mcp.JSONRPCMessage 5 | import org.jetbrains.kotlinx.mcp.PingRequest 6 | import kotlinx.coroutines.CompletableDeferred 7 | import kotlinx.coroutines.runBlocking 8 | import org.jetbrains.kotlinx.mcp.server.StdioServerTransport 9 | import org.junit.jupiter.api.Assertions.* 10 | import org.junit.jupiter.api.BeforeEach 11 | import org.junit.jupiter.api.Test 12 | import org.jetbrains.kotlinx.mcp.shared.ReadBuffer 13 | import org.jetbrains.kotlinx.mcp.shared.serializeMessage 14 | import org.jetbrains.kotlinx.mcp.toJSON 15 | import java.io.* 16 | 17 | class StdioServerTransportTest { 18 | private lateinit var input: PipedInputStream 19 | private lateinit var inputWriter: PipedOutputStream 20 | private lateinit var outputBuffer: ReadBuffer 21 | private lateinit var output: ByteArrayOutputStream 22 | 23 | // We'll store the wrapped streams that meet the constructor requirements 24 | private lateinit var bufferedInput: BufferedInputStream 25 | private lateinit var printOutput: PrintStream 26 | 27 | @BeforeEach 28 | fun setUp() { 29 | // Simulate an input stream that we can push data into using inputWriter. 30 | input = PipedInputStream() 31 | inputWriter = PipedOutputStream(input) 32 | 33 | outputBuffer = ReadBuffer() 34 | 35 | // A custom ByteArrayOutputStream that appends all written data into outputBuffer 36 | output = object : ByteArrayOutputStream() { 37 | override fun write(b: ByteArray, off: Int, len: Int) { 38 | super.write(b, off, len) 39 | outputBuffer.append(b.copyOfRange(off, off + len)) 40 | } 41 | } 42 | 43 | // Wrap input in BufferedInputStream 44 | bufferedInput = BufferedInputStream(input) 45 | 46 | // Wrap output in PrintStream 47 | printOutput = PrintStream(output, true) 48 | } 49 | 50 | @Test 51 | fun `should start then close cleanly`() { 52 | runBlocking { 53 | val server = StdioServerTransport(bufferedInput, printOutput) 54 | server.onError = { error -> 55 | throw error 56 | } 57 | 58 | var didClose = false 59 | server.onClose = { 60 | didClose = true 61 | } 62 | 63 | server.start() 64 | assertFalse(didClose, "Should not have closed yet") 65 | 66 | server.close() 67 | assertTrue(didClose, "Should have closed after calling close()") 68 | } 69 | } 70 | 71 | @Test 72 | fun `should not read until started`() { 73 | runBlocking { 74 | val server = StdioServerTransport(bufferedInput, printOutput) 75 | server.onError = { error -> 76 | throw error 77 | } 78 | 79 | var didRead = false 80 | val readMessage = CompletableDeferred() 81 | 82 | server.onMessage = { message -> 83 | didRead = true 84 | readMessage.complete(message) 85 | } 86 | 87 | val message = PingRequest().toJSON() 88 | 89 | // Push message before the server started 90 | val serialized = serializeMessage(message) 91 | inputWriter.write(serialized) 92 | inputWriter.flush() 93 | 94 | assertFalse(didRead, "Should not have read message before start") 95 | 96 | server.start() 97 | val received = readMessage.await() 98 | assertEquals(message, received) 99 | } 100 | } 101 | 102 | @Test 103 | fun `should read multiple messages`() { 104 | runBlocking { 105 | val server = StdioServerTransport(bufferedInput, printOutput) 106 | server.onError = { error -> 107 | throw error 108 | } 109 | 110 | val messages = listOf( 111 | PingRequest().toJSON(), 112 | InitializedNotification().toJSON(), 113 | ) 114 | 115 | val readMessages = mutableListOf() 116 | val finished = CompletableDeferred() 117 | 118 | server.onMessage = { message -> 119 | readMessages.add(message) 120 | if (message == messages[1]) { 121 | finished.complete(Unit) 122 | } 123 | } 124 | 125 | // Push both messages before starting the server 126 | for (m in messages) { 127 | inputWriter.write(serializeMessage(m)) 128 | } 129 | inputWriter.flush() 130 | 131 | server.start() 132 | finished.await() 133 | 134 | assertEquals(messages, readMessages) 135 | } 136 | } 137 | } 138 | 139 | fun PipedOutputStream.write(s: String) { 140 | write(s.toByteArray()) 141 | } -------------------------------------------------------------------------------- /src/test/kotlin/shared/ReadBufferTest.kt: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import org.jetbrains.kotlinx.mcp.JSONRPCMessage 4 | import org.jetbrains.kotlinx.mcp.JSONRPCNotification 5 | import kotlinx.serialization.encodeToString 6 | import kotlinx.serialization.json.Json 7 | import org.jetbrains.kotlinx.mcp.shared.ReadBuffer 8 | import org.jetbrains.kotlinx.mcp.shared.serializeMessage 9 | import org.junit.jupiter.api.Assertions.assertEquals 10 | import org.junit.jupiter.api.Assertions.assertNull 11 | import org.junit.jupiter.api.Test 12 | import java.nio.charset.StandardCharsets 13 | 14 | class ReadBufferTest { 15 | private val testMessage: JSONRPCMessage = JSONRPCNotification(method = "foobar") 16 | 17 | private val json = Json { 18 | ignoreUnknownKeys = true 19 | encodeDefaults = true 20 | } 21 | 22 | @Test 23 | fun `should have no messages after initialization`() { 24 | val readBuffer = ReadBuffer() 25 | assertNull(readBuffer.readMessage()) 26 | } 27 | 28 | @Test 29 | fun `should only yield a message after a newline`() { 30 | val readBuffer = ReadBuffer() 31 | 32 | // Append message without newline 33 | val messageBytes = json.encodeToString(testMessage) 34 | .toByteArray(StandardCharsets.UTF_8) 35 | readBuffer.append(messageBytes) 36 | assertNull(readBuffer.readMessage()) 37 | 38 | // Append newline and verify message is now available 39 | readBuffer.append("\n".toByteArray(StandardCharsets.UTF_8)) 40 | assertEquals(testMessage, readBuffer.readMessage()) 41 | assertNull(readBuffer.readMessage()) 42 | } 43 | 44 | @Test 45 | fun `should be reusable after clearing`() { 46 | val readBuffer = ReadBuffer() 47 | 48 | readBuffer.append("foobar".toByteArray(Charsets.UTF_8)) 49 | readBuffer.clear() 50 | assertNull(readBuffer.readMessage()) 51 | 52 | val messageJson = serializeMessage(testMessage) 53 | readBuffer.append(messageJson.toByteArray(Charsets.UTF_8)) 54 | readBuffer.append("\n".toByteArray(Charsets.UTF_8)) 55 | val message = readBuffer.readMessage() 56 | assertEquals(testMessage, message) 57 | } 58 | } 59 | --------------------------------------------------------------------------------