Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0e50346
Refactored the model to have the ContentBlock types match the defined…
dstibbe Aug 25, 2025
610b9d6
Updated API (#158)
dstibbe Sep 4, 2025
3d182bc
Added ResourceLink type (#158)
dstibbe Sep 4, 2025
f6c8843
Added ResourceLink serializer (#158)
dstibbe Sep 4, 2025
cf7976d
Added tests (#158)
dstibbe Sep 4, 2025
2e7c450
Updated doc (#158)
dstibbe Sep 4, 2025
8f00711
Updated documentation (#158)
dstibbe Sep 4, 2025
c3746a4
Re-added old types for backwards compatibility (#158)
dstibbe Sep 6, 2025
f2b04d3
Updated tests and updated api (#158)
dstibbe Sep 12, 2025
ae1ead0
Refactored the model to have the ContentBlock types match the defined…
dstibbe Aug 25, 2025
0bc0304
Updated API (#158)
dstibbe Sep 4, 2025
c210a3a
Added ResourceLink type (#158)
dstibbe Sep 4, 2025
72cd564
Added ResourceLink serializer (#158)
dstibbe Sep 4, 2025
56d8681
Added tests (#158)
dstibbe Sep 4, 2025
cebb28d
Updated doc (#158)
dstibbe Sep 4, 2025
c821241
Updated documentation (#158)
dstibbe Sep 4, 2025
7a2ae41
Re-added old types for backwards compatibility (#158)
dstibbe Sep 6, 2025
5136548
Updated tests and updated api (#158)
dstibbe Sep 12, 2025
98247fb
Merge branch '158-support-resource-links' of https://github.com/dstib…
dstibbe Sep 17, 2025
a0ac8a2
Merge remote-tracking branch 'origin/main' into 158-support-resource-…
dstibbe Sep 17, 2025
8858a6c
Merged master (#158)
dstibbe Sep 17, 2025
cfad8a5
Merge branch 'main' into 158-support-resource-links
dstibbe Sep 19, 2025
9a40df1
Merge branch 'main' into 158-support-resource-links
dstibbe Sep 20, 2025
c62db93
Merge branch 'main' into 158-support-resource-links
dstibbe Sep 25, 2025
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to add KDoc to all public methods?


/**
* 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CreateMessageResult supports only TextContent | ImageContent | AudioContent. ContentBlock has also ResourceLink | EmbeddedResource

https://modelcontextprotocol.io/specification/2025-06-18/schema#createmessageresult


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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SamplingMessage supports only TextContent | ImageContent | AudioContent

https://modelcontextprotocol.io/specification/2025-06-18/schema#samplingmessage


/**
* 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