Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ See [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/
* Run `./gradlew assemble` to build the project and produce the corresponding artifacts.
* Run `./gradlew test` to test the module and speed up development.
* Run `./gradlew build` to build the project, which also runs all the tests.
* Run `./gradlew allTests` to run all tests.

*note*: when you change the data model, you might need to regenerate the .api files by running `./gradlew apiDump`.

## Contacting maintainers

Expand Down
118 changes: 87 additions & 31 deletions kotlin-sdk-core/api/kotlin-sdk-core.api

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -970,19 +970,34 @@ public data class GetPromptRequest(
override val method: Method = Method.Defined.PromptsGet
}

/**
* Represents the content of a prompt message.
*/
@Serializable(with = PromptMessageContentPolymorphicSerializer::class)
@Deprecated("For backwards compatibility; use ContentBlock instead", ReplaceWith("ContentBlock"))
public sealed interface PromptMessageContent {
public val type: String
}

@Deprecated(
"For backwards compatibility; use CreateMessageResultContent or SamplingMessageContent instead",
ReplaceWith("CreateMessageResultContent"),
)
public sealed interface PromptMessageContentMultimodal : PromptMessageContent

/**
* Represents prompt message content that is either text, image or audio.
* Represents the types of a ContentBlock
*/
@Serializable(with = PromptMessageContentMultimodalPolymorphicSerializer::class)
public sealed interface PromptMessageContentMultimodal : PromptMessageContent
@Serializable(with = ContentBlockPolymorphicSerializer::class)
public sealed interface ContentBlock : PromptMessageContent

/**
* Represents content for the CreateMessageResult
*/
@Serializable(with = CreateMessageResultContentMultimodalPolymorphicSerializer::class)
public sealed interface CreateMessageResultContent : ContentBlock

/**
* Represents content for the SamplingMessage
*/
@Serializable(with = SamplingMessageContentMultimodalPolymorphicSerializer::class)
public sealed interface SamplingMessageContent : ContentBlock

/**
* Text provided to or from an LLM.
Expand All @@ -998,7 +1013,9 @@ public data class TextContent(
* Optional annotations for the client.
*/
val annotations: Annotations? = null,
) : PromptMessageContentMultimodal {
) : ContentBlock,
CreateMessageResultContent,
SamplingMessageContent {
override val type: String = TYPE

public companion object {
Expand All @@ -1025,7 +1042,9 @@ public data class ImageContent(
* Optional annotations for the client.
*/
val annotations: Annotations? = null,
) : PromptMessageContentMultimodal {
) : ContentBlock,
CreateMessageResultContent,
SamplingMessageContent {
override val type: String = TYPE

public companion object {
Expand All @@ -1052,19 +1071,80 @@ public data class AudioContent(
* Optional annotations for the client.
*/
val annotations: Annotations? = null,
) : PromptMessageContentMultimodal {
) : ContentBlock,
CreateMessageResultContent,
SamplingMessageContent {
override val type: String = TYPE

public companion object {
public const val TYPE: String = "audio"
}
}

/**
* A Resource Link provided to or from an LLM.
*/
@Serializable
public data class ResourceLink(
/**
* A description of what this resource represents.
*
* This can be used by clients to improve the LLM’s understanding of available resources. It can be thought of like a “hint” to the model.
*
*/
val description: String? = null,

/**
* Intended for programmatic or logical use, but used as a display name in past specs or fallback (if title isn’t present).
*/
val name: String,

/**
* The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known.
*
* This can be used by Hosts to display file sizes and estimate context window usage.
*
*/
val size: Long? = null,

/**
* Intended for UI and end-user contexts — optimized to be human-readable and easily understood, even by those unfamiliar with domain-specific terminology.
*
* If not provided, the name should be used for display (except for Tool, where annotations.title should be given precedence over using name, if present).
*
*/
val title: String? = null,

/**
* The URI of this resource.
*/
val uri: String,

/**
* The MIME type of this resource, if known.
*/
val mimeType: String,

/**
* Optional annotations for the client.
*/
val annotations: Annotations? = null,
) : ContentBlock {
override val type: String = TYPE

public companion object {
public const val TYPE: String = "resource_link"
}
}

/**
* Unknown content provided to or from an LLM.
*/
@Serializable
public data class UnknownContent(override val type: String) : PromptMessageContentMultimodal
public data class UnknownContent(override val type: String) :
ContentBlock,
CreateMessageResultContent,
SamplingMessageContent

/**
* The contents of a resource, embedded into a prompt or tool call result.
Expand All @@ -1080,7 +1160,7 @@ public data class EmbeddedResource(
* Optional annotations for the client.
*/
val annotations: Annotations? = null,
) : PromptMessageContent {
) : ContentBlock {
override val type: String = TYPE

public companion object {
Expand Down Expand Up @@ -1130,7 +1210,7 @@ public data class Annotations(
* Describes a message returned as part of a prompt.
*/
@Serializable
public data class PromptMessage(val role: Role, val content: PromptMessageContent)
public data class PromptMessage(val role: Role, val content: ContentBlock)

/**
* The server's response to a prompts/get request from the client.
Expand Down Expand Up @@ -1282,7 +1362,7 @@ public class ListToolsResult(
*/
@Serializable
public sealed interface CallToolResultBase : ServerResult {
public val content: List<PromptMessageContent>
public val content: List<ContentBlock>
public val structuredContent: JsonObject?
public val isError: Boolean? get() = false
}
Expand All @@ -1292,7 +1372,7 @@ public sealed interface CallToolResultBase : ServerResult {
*/
@Serializable
public class CallToolResult(
override val content: List<PromptMessageContent>,
override val content: List<ContentBlock>,
override val structuredContent: JsonObject? = null,
override val isError: Boolean? = false,
override val _meta: JsonObject = EmptyJsonObject,
Expand All @@ -1303,7 +1383,7 @@ public class CallToolResult(
*/
@Serializable
public class CompatibilityCallToolResult(
override val content: List<PromptMessageContent>,
override val content: List<ContentBlock>,
override val structuredContent: JsonObject? = null,
override val isError: Boolean? = false,
override val _meta: JsonObject = EmptyJsonObject,
Expand Down Expand Up @@ -1448,7 +1528,7 @@ public class ModelPreferences(
* Describes a message issued to or received from an LLM API.
*/
@Serializable
public data class SamplingMessage(val role: Role, val content: PromptMessageContentMultimodal)
public data class SamplingMessage(val role: Role, val content: SamplingMessageContent)

/**
* A request from the server to sample an LLM via the client.
Expand Down Expand Up @@ -1530,7 +1610,7 @@ public data class CreateMessageResult(
*/
val stopReason: StopReason? = null,
val role: Role,
val content: PromptMessageContentMultimodal,
val content: CreateMessageResultContent,
override val _meta: JsonObject = EmptyJsonObject,
) : ClientResult

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,21 +81,32 @@ internal object ReferencePolymorphicSerializer : JsonContentPolymorphicSerialize
}
}

internal object PromptMessageContentPolymorphicSerializer :
JsonContentPolymorphicSerializer<PromptMessageContent>(PromptMessageContent::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<PromptMessageContent> =
internal object ContentBlockPolymorphicSerializer :
JsonContentPolymorphicSerializer<ContentBlock>(ContentBlock::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<ContentBlock> =
when (element.jsonObject.getValue("type").jsonPrimitive.content) {
ImageContent.TYPE -> ImageContent.serializer()
TextContent.TYPE -> TextContent.serializer()
EmbeddedResource.TYPE -> EmbeddedResource.serializer()
AudioContent.TYPE -> AudioContent.serializer()
ResourceLink.TYPE -> ResourceLink.serializer()
else -> UnknownContent.serializer()
}
}

internal object PromptMessageContentMultimodalPolymorphicSerializer :
JsonContentPolymorphicSerializer<PromptMessageContentMultimodal>(PromptMessageContentMultimodal::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<PromptMessageContentMultimodal> =
internal object CreateMessageResultContentMultimodalPolymorphicSerializer :
JsonContentPolymorphicSerializer<CreateMessageResultContent>(CreateMessageResultContent::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<CreateMessageResultContent> =
when (element.jsonObject.getValue("type").jsonPrimitive.content) {
ImageContent.TYPE -> ImageContent.serializer()
TextContent.TYPE -> TextContent.serializer()
AudioContent.TYPE -> AudioContent.serializer()
else -> UnknownContent.serializer()
}
}
internal object SamplingMessageContentMultimodalPolymorphicSerializer :
JsonContentPolymorphicSerializer<SamplingMessageContent>(SamplingMessageContent::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<SamplingMessageContent> =
when (element.jsonObject.getValue("type").jsonPrimitive.content) {
ImageContent.TYPE -> ImageContent.serializer()
TextContent.TYPE -> TextContent.serializer()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class TypesTest {
assertEquals("invalid_type", decoded.type)
}

// PromptMessageContent Tests
// ContentBlock Tests
@Test
fun `should validate text content`() {
val textContent = TextContent(text = "Hello, world!")
Expand All @@ -94,8 +94,8 @@ class TypesTest {
fun `should serialize and deserialize text content correctly`() {
val textContent = TextContent(text = "Test message")

val json = McpJson.encodeToString<PromptMessageContent>(textContent)
val decoded = McpJson.decodeFromString<PromptMessageContent>(json)
val json = McpJson.encodeToString<ContentBlock>(textContent)
val decoded = McpJson.decodeFromString<ContentBlock>(json)

assertIs<TextContent>(decoded)
assertEquals("text", decoded.type)
Expand All @@ -121,8 +121,8 @@ class TypesTest {
mimeType = "image/jpeg",
)

val json = McpJson.encodeToString<PromptMessageContent>(imageContent)
val decoded = McpJson.decodeFromString<PromptMessageContent>(json)
val json = McpJson.encodeToString<ContentBlock>(imageContent)
val decoded = McpJson.decodeFromString<ContentBlock>(json)

assertIs<ImageContent>(decoded)
assertEquals("image", decoded.type)
Expand All @@ -149,15 +149,61 @@ class TypesTest {
mimeType = "audio/wav",
)

val json = McpJson.encodeToString<PromptMessageContent>(audioContent)
val decoded = McpJson.decodeFromString<PromptMessageContent>(json)
val json = McpJson.encodeToString<ContentBlock>(audioContent)
val decoded = McpJson.decodeFromString<ContentBlock>(json)

assertIs<AudioContent>(decoded)
assertEquals("audio", decoded.type)
assertEquals("YXVkaW8=", decoded.data)
assertEquals("audio/wav", decoded.mimeType)
}

@Test
fun `should validate resource link content`() {
val resourceLink = ResourceLink(
mimeType = "application/pdf",
description = "This pdf is meant to be a resource link test",
name = "file01",
size = 76859L,
title = "This is a pdf",
uri = "file:///path/to/my_file.pdf",
)

with(resourceLink) {
assertEquals("application/pdf", mimeType)
assertEquals("This pdf is meant to be a resource link test", description)
assertEquals("file01", name)
assertEquals(76859L, size)
assertEquals("This is a pdf", title)
assertEquals("file:///path/to/my_file.pdf", uri)
}
}

@Test
fun `should serialize and deserialize resource link correctly`() {
val resourceLink = ResourceLink(
mimeType = "application/pdf",
description = "This pdf is meant to be a resource link test",
name = "file01",
size = 76859L,
title = "This is a pdf",
uri = "file:///path/to/my_file.pdf",
)

val json = McpJson.encodeToString<ContentBlock>(resourceLink)
val decoded = McpJson.decodeFromString<ContentBlock>(json)

assertIs<ResourceLink>(decoded)
with(decoded) {
assertEquals("application/pdf", mimeType)
assertEquals("This pdf is meant to be a resource link test", description)
assertEquals("file01", name)
assertEquals(76859L, size)
assertEquals("This is a pdf", title)
assertEquals("file:///path/to/my_file.pdf", uri)
}
}

@Test
fun `should validate embedded resource content`() {
val resource = TextResourceContents(
Expand All @@ -180,8 +226,8 @@ class TypesTest {
)
val embeddedResource = EmbeddedResource(resource = resource)

val json = McpJson.encodeToString<PromptMessageContent>(embeddedResource)
val decoded = McpJson.decodeFromString<PromptMessageContent>(json)
val json = McpJson.encodeToString<ContentBlock>(embeddedResource)
val decoded = McpJson.decodeFromString<ContentBlock>(json)

assertIs<EmbeddedResource>(decoded)
assertEquals("resource", decoded.type)
Expand All @@ -196,7 +242,7 @@ class TypesTest {
fun `should handle unknown content type`() {
val unknownJson = """{"type": "unknown_type"}"""

val decoded = McpJson.decodeFromString<PromptMessageContent>(unknownJson)
val decoded = McpJson.decodeFromString<ContentBlock>(unknownJson)

assertIs<UnknownContent>(decoded)
assertEquals("unknown_type", decoded.type)
Expand Down
Loading