Skip to content

Fix for #56 - missing method attribute from client RPC message #67

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

Conversation

IlyaGulya
Copy link

Motivation and Context

This PR fixes issue #56 where some MCP clients (including Cursor) don't include the "method" attribute in the params map of their notification messages. The absence of this attribute caused NPEs during initialization. The solution adds the method parameter to the params object when missing, ensuring compatibility with various clients.

How Has This Been Tested?

I've implemented a test suite that covers all potential scenarios:

  • When method is missing from JsonObject params
  • When method already exists in JsonObject params
  • When params is a JsonArray (should remain unmodified)
  • When params is JsonNull (should remain JsonNull)
  • Testing the fallback notification handler behavior
  • Error handling when a handler throws an exception

Breaking Changes

None

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the [MCP Documentation](https://modelcontextprotocol.io)
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

This PR builds upon the initial work by @tomakehurst to fix issue #56.
It retains the fix while adding test coverage and removing unintended version changes as requested by the reviewer.

@e5l e5l requested review from Copilot and e5l and removed request for Copilot April 7, 2025 08:44
@e5l e5l self-assigned this Apr 7, 2025
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

Comments suppressed due to low confidence (1)

src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/TestTransport.kt:1

  • The package name appears to be duplicated; consider changing it to 'io.modelcontextprotocol.kotlin.sdk.shared' to align with the directory structure and avoid confusion.
package io.modelcontextprotocol.kotlin.sdk.io.modelcontextprotocol.kotlin.sdk.shared

@IlyaGulya IlyaGulya force-pushed the candidate-rpc-method-param-fix branch from 7637f9c to 7006ce9 Compare April 7, 2025 09:16
@IlyaGulya
Copy link
Author

Fixed package for TestTransport, thanks for noticing 🙂

@tomakehurst
Copy link

The odd thing is that the latest release seems to have somehow fixed this particular problem despite not including my PR or something like it. I haven't had a chance to debug it and find out what's happening, but it might be worth examining before merging this.

@IlyaGulya
Copy link
Author

Hmm, I will check, because in my case this issue still was present

@IlyaGulya
Copy link
Author

Yeah, seems to be fixed for me as well. There's another bug left but I will make a separate PR for it, I suppose.

21:05:43.651 [DefaultDispatcher-worker-2] ERROR io.modelcontextprotocol.kotlin.sdk.shared.Protocol -- Error handling notification: notifications/initialized
java.util.NoSuchElementException: Key method is missing in the map.
	at kotlin.collections.MapsKt__MapWithDefaultKt.getOrImplicitDefaultNullable(MapWithDefault.kt:24)
	at kotlin.collections.MapsKt__MapsKt.getValue(Maps.kt:372)
	at io.modelcontextprotocol.kotlin.sdk.Types_utilKt.selectClientNotificationDeserializer(types.util.kt:137)
	at io.modelcontextprotocol.kotlin.sdk.Types_utilKt.access$selectClientNotificationDeserializer(types.util.kt:1)
	at io.modelcontextprotocol.kotlin.sdk.NotificationPolymorphicSerializer.selectDeserializer(types.util.kt:187)
	at kotlinx.serialization.json.JsonContentPolymorphicSerializer.deserialize(JsonContentPolymorphicSerializer.kt:93)
	at kotlinx.serialization.json.internal.AbstractJsonTreeDecoder.decodeSerializableValue(TreeJsonDecoder.kt:337)
	at kotlinx.serialization.json.internal.TreeJsonDecoderKt.readJson(TreeJsonDecoder.kt:26)
	at kotlinx.serialization.json.Json.decodeFromJsonElement(Json.kt:186)
	at io.modelcontextprotocol.kotlin.sdk.TypesKt.fromJSON(types.kt:1461)
	at io.modelcontextprotocol.kotlin.sdk.shared.Protocol$setNotificationHandler$1.invokeSuspend(Protocol.kt:476)
	at io.modelcontextprotocol.kotlin.sdk.shared.Protocol$setNotificationHandler$1.invoke(Protocol.kt)
	at io.modelcontextprotocol.kotlin.sdk.shared.Protocol$setNotificationHandler$1.invoke(Protocol.kt)
	at io.modelcontextprotocol.kotlin.sdk.shared.Protocol.onNotification(Protocol.kt:198)
	at io.modelcontextprotocol.kotlin.sdk.shared.Protocol.access$onNotification(Protocol.kt:93)
	at io.modelcontextprotocol.kotlin.sdk.shared.Protocol$connect$4.invokeSuspend(Protocol.kt:167)
	at io.modelcontextprotocol.kotlin.sdk.shared.Protocol$connect$4.invoke(Protocol.kt)
	at io.modelcontextprotocol.kotlin.sdk.shared.Protocol$connect$4.invoke(Protocol.kt)
	at io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport$onMessage$1.invokeSuspend(Transport.kt:94)
	at io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport$onMessage$1.invoke(Transport.kt)
	at io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport$onMessage$1.invoke(Transport.kt)
	at io.modelcontextprotocol.kotlin.sdk.server.SseServerTransport.handleMessage(SSEServerTransport.kt:102)
	at io.modelcontextprotocol.kotlin.sdk.server.SseServerTransport.handlePostMessage(SSEServerTransport.kt:86)
	at io.modelcontextprotocol.kotlin.sdk.server.KtorServerKt.mcpPostEndpoint(KtorServer.kt:108)
	at io.modelcontextprotocol.kotlin.sdk.server.KtorServerKt$mcp$4$2.invokeSuspend(KtorServer.kt:55)
	at io.modelcontextprotocol.kotlin.sdk.server.KtorServerKt$mcp$4$2.invoke(KtorServer.kt)
	at io.modelcontextprotocol.kotlin.sdk.server.KtorServerKt$mcp$4$2.invoke(KtorServer.kt)
	at io.ktor.server.routing.RoutingNode$buildPipeline$1$1.invokeSuspend(RoutingNode.kt:116)
	at io.ktor.server.routing.RoutingNode$buildPipeline$1$1.invoke(RoutingNode.kt)
	at io.ktor.server.routing.RoutingNode$buildPipeline$1$1.invoke(RoutingNode.kt)
	at io.ktor.util.pipeline.PipelineJvmKt.pipelineStartCoroutineUninterceptedOrReturn(PipelineJvm.kt:15)
	at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:131)
	at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:89)
	at io.ktor.util.pipeline.SuspendFunctionGun.execute$ktor_utils(SuspendFunctionGun.kt:109)
	at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:86)
	at io.ktor.server.routing.RoutingRoot$executeResult$$inlined$execute$1.invokeSuspend(Pipeline.kt:488)
	at io.ktor.server.routing.RoutingRoot$executeResult$$inlined$execute$1.invoke(Pipeline.kt)
	at io.ktor.server.routing.RoutingRoot$executeResult$$inlined$execute$1.invoke(Pipeline.kt)
	at io.ktor.util.debug.ContextUtilsKt.initContextInDebugMode(ContextUtils.kt:17)
	at io.ktor.server.routing.RoutingRoot.executeResult(RoutingRoot.kt:193)
	at io.ktor.server.routing.RoutingRoot.interceptor(RoutingRoot.kt:66)
	at io.ktor.server.routing.RoutingRoot$Plugin$install$1.invokeSuspend(RoutingRoot.kt:143)
	at io.ktor.server.routing.RoutingRoot$Plugin$install$1.invoke(RoutingRoot.kt)
	at io.ktor.server.routing.RoutingRoot$Plugin$install$1.invoke(RoutingRoot.kt)
	at io.ktor.util.pipeline.PipelineJvmKt.pipelineStartCoroutineUninterceptedOrReturn(PipelineJvm.kt:15)
	at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:131)
	at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:89)
	at io.ktor.server.engine.BaseApplicationEngineKt$installDefaultTransformationChecker$1.invokeSuspend(BaseApplicationEngine.kt:112)
	at io.ktor.server.engine.BaseApplicationEngineKt$installDefaultTransformationChecker$1.invoke(BaseApplicationEngine.kt)
	at io.ktor.server.engine.BaseApplicationEngineKt$installDefaultTransformationChecker$1.invoke(BaseApplicationEngine.kt)
	at io.ktor.util.pipeline.PipelineJvmKt.pipelineStartCoroutineUninterceptedOrReturn(PipelineJvm.kt:15)
	at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:131)
	at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:89)
	at io.ktor.util.pipeline.SuspendFunctionGun.execute$ktor_utils(SuspendFunctionGun.kt:109)
	at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:86)
	at io.ktor.server.engine.DefaultEnginePipelineKt$defaultEnginePipeline$1$invokeSuspend$$inlined$execute$1.invokeSuspend(Pipeline.kt:488)
	at io.ktor.server.engine.DefaultEnginePipelineKt$defaultEnginePipeline$1$invokeSuspend$$inlined$execute$1.invoke(Pipeline.kt)
	at io.ktor.server.engine.DefaultEnginePipelineKt$defaultEnginePipeline$1$invokeSuspend$$inlined$execute$1.invoke(Pipeline.kt)
	at io.ktor.util.debug.ContextUtilsKt.initContextInDebugMode(ContextUtils.kt:17)
	at io.ktor.server.engine.DefaultEnginePipelineKt$defaultEnginePipeline$1.invokeSuspend(DefaultEnginePipeline.kt:123)
	at io.ktor.server.engine.DefaultEnginePipelineKt$defaultEnginePipeline$1.invoke(DefaultEnginePipeline.kt)
	at io.ktor.server.engine.DefaultEnginePipelineKt$defaultEnginePipeline$1.invoke(DefaultEnginePipeline.kt)
	at io.ktor.util.pipeline.PipelineJvmKt.pipelineStartCoroutineUninterceptedOrReturn(PipelineJvm.kt:15)
	at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:131)
	at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:89)
	at io.ktor.util.pipeline.SuspendFunctionGun.execute$ktor_utils(SuspendFunctionGun.kt:109)
	at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:86)
	at io.ktor.server.cio.CIOApplicationEngine$handleRequest$2$invokeSuspend$$inlined$execute$1.invokeSuspend(Pipeline.kt:488)
	at io.ktor.server.cio.CIOApplicationEngine$handleRequest$2$invokeSuspend$$inlined$execute$1.invoke(Pipeline.kt)
	at io.ktor.server.cio.CIOApplicationEngine$handleRequest$2$invokeSuspend$$inlined$execute$1.invoke(Pipeline.kt)
	at io.ktor.util.debug.ContextUtilsKt.initContextInDebugMode(ContextUtils.kt:17)
	at io.ktor.server.cio.CIOApplicationEngine$handleRequest$2.invokeSuspend(CIOApplicationEngine.kt:229)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:101)
	at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:113)
	at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:89)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:589)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:823)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:720)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:707)

@IlyaGulya IlyaGulya closed this Apr 7, 2025
@apikas
Copy link

apikas commented Apr 9, 2025

Well done, @tomakehurst and @IlyaGulya !
I have built both of your commits locally (06597f3 and 7006ce9), and they both solve the problem I saw in 0.3.0 and 0.4.0 (MCP clients (including Cursor) don't include the "method" attribute in the params map of their notification messages).
I have tried my own MCP server build on the kotlin-sdk with both Claude Code 0.2.66 and MCP Inspector v0.8.1.
My opinion is that this is a showstopper, and that it would be very nice if a fix is included in the next release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants