Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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