Skip to content

Fix CallToolRequest deserialization in McpAsyncServer #355

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

cobach
Copy link

@cobach cobach commented Jun 29, 2025

Summary

This PR fixes the CallToolRequest deserialization issue in McpAsyncServer.toolsCallRequestHandler() where the handler was expecting params to be an instance of CallToolRequest, but JSON-RPC deserialization provides it as a Map (specifically LinkedHashMap).

Problem

When calling tools via MCP, the following error occurs:

class java.util.LinkedHashMap cannot be cast to class io.modelcontextprotocol.spec.McpSchema$CallToolRequest

This happens because:

  1. The JSON-RPC layer deserializes the params object as a generic Map
  2. The handler was attempting to cast this Map directly to CallToolRequest
  3. This cast always fails, making tool calls unusable

Solution

Added proper type checking and conversion in the toolsCallRequestHandler():

  • Check if params is already a CallToolRequest (unlikely but safe)
  • Check if params is a Map and convert it using ObjectMapper.convertValue()
  • Handle other types with a generic conversion attempt
  • Provide clear error messages for conversion failures

Testing

Tested with the Node.js MCP SDK client:

const result = await client.callTool({ 
  name: 'SystemInformation', 
  arguments: {} 
});

Before fix: ClassCastException
After fix: Tool executes successfully and returns expected results

Related Issues

Fixes #354

The toolsCallRequestHandler was expecting params to be an instance of
CallToolRequest, but JSON-RPC deserialization provides it as a Map
(LinkedHashMap). This caused a ClassCastException when trying to cast
the params directly.

This fix adds proper type checking and conversion:
- Check if params is already a CallToolRequest (unlikely but safe)
- Check if params is a Map and convert it using ObjectMapper
- Handle other types with a generic conversion attempt

The issue occurs because the JSON-RPC layer deserializes the params
object as a generic Map, not as the specific CallToolRequest type.
The handler needs to explicitly convert this Map to the expected type.

Fixes #354
@tzolov
Copy link
Contributor

tzolov commented Jun 30, 2025

@cobach, I'm not sure this is an issue, or at least not on the Java SDK side.

For example this works fine:

        Object params1 = new McpSchema.CallToolRequest("calculator", Map.of(
                "operation", "2 + 3",
                "timestamp", "2024-01-01T10:00:00Z"));

        McpSchema.CallToolRequest callToolRequest1 = new ObjectMapper().convertValue(params1,
                new TypeReference<McpSchema.CallToolRequest>() {
                });

        System.out.println("CallToolRequest 1: " + callToolRequest1);

        Object params2 = Map.of("name", "calculator", "arguments", Map.of(
                "operation", "2 + 3",
                "timestamp", "2024-01-01T10:00:00Z"));

        McpSchema.CallToolRequest callToolRequest2 = new ObjectMapper().convertValue(params2,
                new TypeReference<McpSchema.CallToolRequest>() {
                });

        System.out.println("CallToolRequest 2: " + callToolRequest2);

        System.out.println("Compare 1&2: " + callToolRequest1.equals(callToolRequest2));

        Object params3 = Map.of("name", "calculator", "arguments", Map.of());

        McpSchema.CallToolRequest callToolRequest3 = new ObjectMapper().convertValue(params3,
                new TypeReference<McpSchema.CallToolRequest>() {
                });

        System.out.println("CallToolRequest 3: " + callToolRequest3);

Please provide a runnable Test to verify this problem.

@cobach
Copy link
Author

cobach commented Jun 30, 2025

@tzolov Thank you for your review! I understand your concern, and you're right that ObjectMapper.convertValue() can handle the conversion. However, the issue is more subtle and relates to how the JSON-RPC layer deserializes the incoming request.

The key difference between your example and the actual runtime scenario:

  1. Your example: You start with already-typed Java objects (either CallToolRequest or Map)
  2. Actual runtime: The JSON-RPC layer deserializes the incoming JSON into a LinkedHashMap

Here's a test case that reproduces the actual issue:

@Test
public void testActualJsonRpcDeserialization() throws Exception {
    ObjectMapper objectMapper = new ObjectMapper();
    
    // This is the actual JSON that comes from Node.js MCP SDK client
    String jsonFromWire = """
        {
            "jsonrpc": "2.0",
            "method": "tools/call",
            "params": {
                "name": "calculator",
                "arguments": {
                    "operation": "2 + 3",
                    "timestamp": "2024-01-01T10:00:00Z"
                }
            },
            "id": "123"
        }
        """;
    
    // The JSON-RPC layer deserializes this to a Map
    Map<String, Object> jsonRpcMessage = objectMapper.readValue(jsonFromWire, 
        new TypeReference<Map<String, Object>>() {});
    
    Object params = jsonRpcMessage.get("params");
    
    // This shows the actual type at runtime
    System.out.println("Actual params type: " + params.getClass().getName());
    // Output: Actual params type: java.util.LinkedHashMap
    
    // The current code attempts this direct cast, which fails
    try {
        McpSchema.CallToolRequest directCast = (McpSchema.CallToolRequest) params;
        fail("This should throw ClassCastException");
    } catch (ClassCastException e) {
        System.out.println("ClassCastException as expected: " + e.getMessage());
    }
    
    // The fix uses convertValue to handle the LinkedHashMap
    McpSchema.CallToolRequest converted = objectMapper.convertValue(params,
        new TypeReference<McpSchema.CallToolRequest>() {});
    
    assertEquals("calculator", converted.name());
    assertNotNull(converted.arguments());
}

The issue occurs because:

  1. The JSON-RPC request comes from the wire as a string
  2. The JSON-RPC layer deserializes it to Map<String, Object>
  3. The params field is a LinkedHashMap, not a CallToolRequest
  4. The current code tries to cast Object (which is actually LinkedHashMap) directly to CallToolRequest

Your example works because you're starting with properly typed objects, but in the actual runtime flow, we're dealing with the result of JSON deserialization, which produces LinkedHashMap instances.

This fix ensures compatibility with any MCP client that sends standard JSON-RPC requests, particularly the Node.js MCP SDK.

@tzolov
Copy link
Contributor

tzolov commented Jul 1, 2025

@cobach, the code you've shared, makes assumptions but doesn't test the existing MCP functionality to show real problem.
Further more we are using the covertValue.

Please create a test that runs agains the actual MCP code and fails.

@cobach
Copy link
Author

cobach commented Jul 1, 2025

Closing this PR - Our apologies

I'm closing this PR as it appears the issue may not exist or may have been specific to our implementation.

@tzolov - I sincerely apologize for not providing a proper test case that demonstrates the issue against the actual MCP code. You were right to ask for a runnable test.

What we found:

After creating a comprehensive test following your feedback:

// Test sends params as Map (like Node.js SDK does)
Map<String, Object> callToolParams = Map.of(
    "name", "test_tool",
    "arguments", Map.of("message", "Hello from test")
);

// The SDK handles this correctly without any ClassCastException

The test passes successfully, showing:

  • The tool handler receives params as LinkedHashMap (as expected from JSON deserialization)
  • No ClassCastException occurs
  • The SDK appears to handle the type conversion correctly

Conclusion:

The issue we experienced was likely in our application code or how we were using the SDK, not in the SDK itself. Without a reproducible test case demonstrating the problem in the SDK, this PR should not be merged.

Thank you for your patience and for insisting on proper test cases. This has been another valuable lesson about the importance of reproducing issues in isolation before proposing fixes.

We'll ensure any future PRs include proper, runnable tests that demonstrate the issue.

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.

CallToolRequest Deserialization Error in McpAsyncServer
2 participants