Skip to content

Update notification schema #199

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 28, 2025
Merged
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
425 changes: 372 additions & 53 deletions api/kotlin-sdk.api

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,7 @@ public open class Server(
* @param params The logging message notification parameters.
*/
public suspend fun sendLoggingMessage(params: LoggingMessageNotification) {
logger.trace { "Sending logging message: ${params.data}" }
logger.trace { "Sending logging message: ${params.params.data}" }
notification(params)
}

Expand All @@ -568,7 +568,7 @@ public open class Server(
* @param params Details of the updated resource.
*/
public suspend fun sendResourceUpdated(params: ResourceUpdatedNotification) {
logger.debug { "Sending resource updated notification for: ${params.uri}" }
logger.debug { "Sending resource updated notification for: ${params.params.uri}" }
notification(params)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,11 +292,11 @@ public abstract class Protocol(
}

private fun onProgress(notification: ProgressNotification) {
LOGGER.trace { "Received progress notification: token=${notification.progressToken}, progress=${notification.progress}/${notification.total}" }
val progress = notification.progress
val total = notification.total
val message = notification.message
val progressToken = notification.progressToken
LOGGER.trace { "Received progress notification: token=${notification.params.progressToken}, progress=${notification.params.progress}/${notification.params.total}" }
val progress = notification.params.progress
val total = notification.params.total
val message = notification.params.message
val progressToken = notification.params.progressToken

val handler = _progressHandlers.value[progressToken]
if (handler == null) {
Expand Down Expand Up @@ -424,7 +424,12 @@ public abstract class Protocol(
_responseHandlers.update { current -> current.remove(messageId) }
_progressHandlers.update { current -> current.remove(messageId) }

val notification = CancelledNotification(requestId = messageId, reason = reason.message ?: "Unknown")
val notification = CancelledNotification(
params = CancelledNotification.Params(
requestId = messageId,
reason = reason.message ?: "Unknown"
)
)

val serialized = JSONRPCNotification(
notification.method.value,
Expand Down
194 changes: 133 additions & 61 deletions src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.encodeToJsonElement
import kotlinx.serialization.json.jsonObject
Expand Down Expand Up @@ -125,10 +126,11 @@ public sealed interface Request {
* @return The JSON-RPC request representation.
*/
internal fun Request.toJSON(): JSONRPCRequest {
val encoded = JsonObject(McpJson.encodeToJsonElement(this).jsonObject.minus("method"))
val fullJson = McpJson.encodeToJsonElement(this).jsonObject
val params = JsonObject(fullJson.filterKeys { it != "method" })
return JSONRPCRequest(
method = method.value,
params = encoded,
params = params,
jsonrpc = JSONRPC_VERSION,
)
}
Expand All @@ -139,8 +141,7 @@ internal fun Request.toJSON(): JSONRPCRequest {
* @return The decoded [Request] or null
*/
internal fun JSONRPCRequest.fromJSON(): Request {
val requestData = JsonObject(params.jsonObject.plus("method" to JsonPrimitive(method)))

val requestData = JsonObject(params.jsonObject + ("method" to JsonPrimitive(method)))
val deserializer = selectRequestDeserializer(method)
return McpJson.decodeFromJsonElement(deserializer, requestData)
}
Expand All @@ -159,6 +160,7 @@ public open class CustomRequest(override val method: Method) : Request
@Serializable(with = NotificationPolymorphicSerializer::class)
public sealed interface Notification {
public val method: Method
public val params: NotificationParams?
}

/**
Expand All @@ -167,10 +169,9 @@ public sealed interface Notification {
* @return The JSON-RPC notification representation.
*/
internal fun Notification.toJSON(): JSONRPCNotification {
val encoded = JsonObject(McpJson.encodeToJsonElement<Notification>(this).jsonObject.minus("method"))
return JSONRPCNotification(
method.value,
params = encoded
method = method.value,
params = McpJson.encodeToJsonElement(params),
Comment on lines 172 to +174
Copy link
Preview

Copilot AI Jul 25, 2025

Choose a reason for hiding this comment

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

The serialization logic for notifications doesn't handle null params correctly. When params is null, encoding it will create a JSON null value, but the protocol may expect the params field to be omitted entirely.

Copilot uses AI. Check for mistakes.

)
}

Expand All @@ -180,7 +181,10 @@ internal fun Notification.toJSON(): JSONRPCNotification {
* @return The decoded [Notification].
*/
internal fun JSONRPCNotification.fromJSON(): Notification {
val data = JsonObject(params.jsonObject.plus("method" to JsonPrimitive(method)))
val data = buildJsonObject {
put("method", JsonPrimitive(method))
put("params", params)
}
return McpJson.decodeFromJsonElement<Notification>(data)
}

Expand Down Expand Up @@ -295,6 +299,12 @@ public data class JSONRPCError(
val data: JsonObject = EmptyJsonObject,
) : JSONRPCMessage

/**
* Base interface for notification parameters with optional metadata.
*/
@Serializable
public sealed interface NotificationParams : WithMeta

/* Cancellation */
/**
* This notification can be sent by either side to indicate that it is cancelling a previously issued request.
Expand All @@ -307,19 +317,24 @@ public data class JSONRPCError(
*/
@Serializable
public data class CancelledNotification(
/**
* The ID of the request to cancel.
*
* It MUST correspond to the ID of a request previously issued in the same direction.
*/
val requestId: RequestId,
/**
* An optional string describing the reason for the cancellation. This MAY be logged or presented to the user.
*/
val reason: String?,
override val _meta: JsonObject = EmptyJsonObject,
) : ClientNotification, ServerNotification, WithMeta {
override val params: Params,
) : ClientNotification, ServerNotification {
override val method: Method = Method.Defined.NotificationsCancelled

@Serializable
public data class Params(
/**
* The ID of the request to cancel.
*
* It MUST correspond to the ID of a request previously issued in the same direction.
*/
val requestId: RequestId,
/**
* An optional string describing the reason for the cancellation. This MAY be logged or presented to the user.
*/
val reason: String? = null,
override val _meta: JsonObject = EmptyJsonObject,
) : NotificationParams
}

/* Initialization */
Expand Down Expand Up @@ -408,7 +423,7 @@ public sealed interface ServerResult : RequestResult
*/
@Serializable
public data class UnknownMethodRequestOrNotification(
override val method: Method,
override val method: Method, override val params: NotificationParams? = null,
Copy link
Preview

Copilot AI Jul 25, 2025

Choose a reason for hiding this comment

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

Making params nullable in UnknownMethodRequestOrNotification but required in other notification types creates inconsistency in the API. Consider whether params should be consistently required or nullable across all notification types.

Suggested change
override val method: Method, override val params: NotificationParams? = null,
override val method: Method, override val params: NotificationParams = DefaultNotificationParams,

Copilot uses AI. Check for mistakes.

) : ClientNotification, ClientRequest, ServerNotification, ServerRequest

/**
Expand Down Expand Up @@ -506,8 +521,15 @@ public data class InitializeResult(
* This notification is sent from the client to the server after initialization has finished.
*/
@Serializable
public class InitializedNotification : ClientNotification {
public data class InitializedNotification(
override val params: Params = Params(),
) : ClientNotification {
override val method: Method = Method.Defined.NotificationsInitialized

@Serializable
public data class Params(
override val _meta: JsonObject = EmptyJsonObject,
) : NotificationParams
}

/* Ping */
Expand All @@ -528,7 +550,7 @@ public sealed interface ProgressBase {
/**
* The progress thus far. This should increase every time progress is made, even if the total is unknown.
*/
public val progress: Int
public val progress: Double

/**
* Total number of items to a process (or total progress required), if known.
Expand All @@ -553,7 +575,7 @@ public open class Progress(
/**
* The progress thus far. This should increase every time progress is made, even if the total is unknown.
*/
override val progress: Int,
override val progress: Double,

/**
* Total number of items to a process (or total progress required), if known.
Expand All @@ -571,18 +593,32 @@ public open class Progress(
*/
@Serializable
public data class ProgressNotification(
override val progress: Int,
/**
* The progress token,
* which was given in the initial request,
* used to associate this notification with the request that is proceeding.
*/
public val progressToken: ProgressToken,
@Suppress("PropertyName") val _meta: JsonObject = EmptyJsonObject,
override val total: Double?,
override val message: String?,
) : ClientNotification, ServerNotification, ProgressBase {
override val params: Params,
) : ClientNotification, ServerNotification {
override val method: Method = Method.Defined.NotificationsProgress

@Serializable
public data class Params(
/**
* The progress thus far. This should increase every time progress is made, even if the total is unknown.
*/
override val progress: Double,
/**
* The progress token,
* which was given in the initial request,
* used to associate this notification with the request that is proceeding.
*/
val progressToken: ProgressToken,
/**
* Total number of items to process (or total progress required), if known.
*/
override val total: Double? = null,
/**
* An optional message describing the current progress.
*/
override val message: String? = null,
override val _meta: JsonObject = EmptyJsonObject,
) : NotificationParams, ProgressBase
}

/* Pagination */
Expand Down Expand Up @@ -784,8 +820,15 @@ public class ReadResourceResult(
* Servers may issue this without any previous subscription from the client.
*/
@Serializable
public class ResourceListChangedNotification : ServerNotification {
public data class ResourceListChangedNotification(
override val params: Params = Params(),
) : ServerNotification {
override val method: Method = Method.Defined.NotificationsResourcesListChanged

@Serializable
public data class Params(
override val _meta: JsonObject = EmptyJsonObject,
) : NotificationParams
}

/**
Expand Down Expand Up @@ -821,13 +864,18 @@ public data class UnsubscribeRequest(
*/
@Serializable
public data class ResourceUpdatedNotification(
/**
* The URI of the resource that has been updated. This might be a sub-resource of the one that the client actually subscribed to.
*/
val uri: String,
override val _meta: JsonObject = EmptyJsonObject,
) : ServerNotification, WithMeta {
override val params: Params,
) : ServerNotification {
override val method: Method = Method.Defined.NotificationsResourcesUpdated

@Serializable
public data class Params(
/**
* The URI of the resource that has been updated. This might be a sub-resource of the one that the client actually subscribed to.
*/
val uri: String,
override val _meta: JsonObject = EmptyJsonObject,
) : NotificationParams
}

/* Prompts */
Expand Down Expand Up @@ -1044,8 +1092,15 @@ public class GetPromptResult(
* Servers may issue this without any previous subscription from the client.
*/
@Serializable
public class PromptListChangedNotification : ServerNotification {
public data class PromptListChangedNotification(
override val params: Params = Params(),
) : ServerNotification {
override val method: Method = Method.Defined.NotificationsPromptsListChanged

@Serializable
public data class Params(
override val _meta: JsonObject = EmptyJsonObject,
) : NotificationParams
}

/* Tools */
Expand Down Expand Up @@ -1223,8 +1278,15 @@ public data class CallToolRequest(
* Servers may issue this without any previous subscription from the client.
*/
@Serializable
public class ToolListChangedNotification : ServerNotification {
public data class ToolListChangedNotification(
override val params: Params = Params(),
) : ServerNotification {
override val method: Method = Method.Defined.NotificationsToolsListChanged

@Serializable
public data class Params(
override val _meta: JsonObject = EmptyJsonObject,
) : NotificationParams
}

/* Logging */
Expand Down Expand Up @@ -1252,22 +1314,27 @@ public enum class LoggingLevel {
*/
@Serializable
public data class LoggingMessageNotification(
/**
* The severity of this log message.
*/
val level: LoggingLevel,
override val params: Params,
) : ServerNotification {
override val method: Method = Method.Defined.NotificationsMessage

/**
* An optional name of the logger issuing this message.
*/
val logger: String? = null,
@Serializable
public data class Params(
/**
* The severity of this log message.
*/
val level: LoggingLevel,
/**
* An optional name of the logger issuing this message.
*/
val logger: String? = null,
/**
* The data to be logged, such as a string message or an object. Any JSON serializable type is allowed here.
*/
val data: JsonElement,
override val _meta: JsonObject = EmptyJsonObject,
) : NotificationParams

/**
* The data to be logged, such as a string message or an object. Any JSON serializable type is allowed here.
*/
val data: JsonObject = EmptyJsonObject,
override val _meta: JsonObject = EmptyJsonObject,
) : ServerNotification, WithMeta {
/**
* A request from the client to the server to enable or adjust logging.
*/
Expand All @@ -1281,8 +1348,6 @@ public data class LoggingMessageNotification(
) : ClientRequest, WithMeta {
override val method: Method = Method.Defined.LoggingSetLevel
}

override val method: Method = Method.Defined.NotificationsMessage
}

/* Sampling */
Expand Down Expand Up @@ -1578,8 +1643,15 @@ public class ListRootsResult(
* A notification from the client to the server, informing it that the list of roots has changed.
*/
@Serializable
public class RootsListChangedNotification : ClientNotification {
public data class RootsListChangedNotification(
override val params: Params = Params(),
) : ClientNotification {
override val method: Method = Method.Defined.NotificationsRootsListChanged

@Serializable
public data class Params(
override val _meta: JsonObject = EmptyJsonObject,
) : NotificationParams
}

/**
Expand Down
Loading
Loading