Skip to content

Commit 4ee89c4

Browse files
armishclaude
andcommitted
Fix MCP Inspector tools/list validation errors
Resolve "Expected object, received null" errors when listing tools in MCP Inspector: - Update stdio-wrapper.py JSON fixing logic: - Add parent_key context to fix_r_json function - Ensure 'properties' field is never null, always empty object {} - Preserve arrays for specific MCP keys (tools, resources, content, etc.) - Keep empty dicts as {} instead of converting to null - Enhance HTTP transport JSON serialization: - Add auto_unbox=TRUE to serializer_json() for consistent output - Ensures proper JSON structure for MCP Inspector compatibility - Add comprehensive MCP Inspector documentation to README: - Instructions for both HTTP and stdio transport usage - Troubleshooting guide for connection issues - Clear setup steps for testing with MCP Inspector - Verified native stdio support remains unaffected: - All tools/list responses have proper inputSchema.properties: {} - Resources functionality fully preserved - 129 tests continue to pass These fixes ensure MCP Inspector can properly validate and display tools while maintaining full compatibility with native MCP clients. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent d8e5dad commit 4ee89c4

3 files changed

Lines changed: 50 additions & 12 deletions

File tree

R/pr_mcp.R

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ pr_mcp_http <- function(pr,
7878

7979
# Mount MCP endpoints
8080
pr %>%
81-
pr_post(paste0(path, "/messages"), mcp_handler$handle_message, serializer = serializer_json()) %>%
82-
pr_get(path, mcp_handler$server_info, serializer = serializer_json())
81+
pr_post(paste0(path, "/messages"), mcp_handler$handle_message, serializer = serializer_json(auto_unbox = TRUE)) %>%
82+
pr_get(path, mcp_handler$server_info, serializer = serializer_json(auto_unbox = TRUE))
8383

8484
invisible(pr)
8585
}

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,13 +371,48 @@ This package implements the [Model Context Protocol](https://modelcontextprotoco
371371
- **Tool Execution**: Converts MCP tool calls to Plumber endpoint requests
372372
- **Error Handling**: Properly formats errors in MCP response format
373373

374+
## Testing with MCP Inspector
375+
376+
[MCP Inspector](https://github.com/modelcontextprotocol/inspector) is a tool for testing and debugging MCP servers.
377+
378+
### Using MCP Inspector with HTTP Transport
379+
380+
1. Start your plumber API with HTTP transport:
381+
```r
382+
library(plumber)
383+
library(plumber2mcp)
384+
385+
pr <- pr("api.R") %>%
386+
pr_mcp(transport = "http") %>%
387+
pr_run(port = 8000)
388+
```
389+
390+
2. In a new terminal, navigate to the examples directory:
391+
```bash
392+
cd /path/to/plumber2mcp/inst/examples
393+
mcp-inspector --config http_wrapper_config.json --server plumber2mcp
394+
```
395+
396+
The `stdio-wrapper.py` script bridges MCP Inspector's stdio interface to your HTTP server.
397+
398+
### Using MCP Inspector with Stdio Transport
399+
400+
Use the stdio configuration directly:
401+
```bash
402+
cd /path/to/plumber2mcp/inst/examples
403+
mcp-inspector --config stdio_config.json --server plumber2mcp
404+
```
405+
374406
## Troubleshooting
375407

376408
### Common Issues
377409

378410
1. **Port already in use**: Change the port number in `pr_run(port = 8001)`
379411
2. **MCP endpoint not found**: Ensure you called `pr_mcp()` before `pr_run()`
380412
3. **Tools not showing up**: Check that your Plumber endpoints have proper annotations
413+
4. **MCP Inspector connection error**:
414+
- For HTTP: Ensure the server is running on port 8000 before starting MCP Inspector
415+
- Check that the `cwd` path in config files points to the correct directory
381416

382417
### Debug Mode
383418

inst/examples/stdio-wrapper.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,24 @@
1111

1212
MCP_ENDPOINT = "http://localhost:8000/mcp/messages"
1313

14-
def fix_r_json(obj):
14+
def fix_r_json(obj, parent_key=None):
1515
"""Fix all R jsonlite serialization issues"""
1616
if obj is None:
17+
# Special case: properties should never be null, always empty object
18+
if parent_key == "properties":
19+
return {}
1720
return None
1821
elif isinstance(obj, list):
19-
if len(obj) == 1:
20-
# Convert single-element arrays to scalars
21-
return fix_r_json(obj[0])
22+
if len(obj) == 1 and parent_key not in ["tools", "resources", "content", "required", "resourceTemplates"]:
23+
# Convert single-element arrays to scalars (except for specific keys that should remain arrays)
24+
return fix_r_json(obj[0], parent_key)
2225
else:
2326
# Process array elements
24-
return [fix_r_json(item) for item in obj]
27+
return [fix_r_json(item, parent_key) for item in obj]
2528
elif isinstance(obj, dict):
2629
if len(obj) == 0:
27-
# Empty dict - context dependent
28-
return None # Most cases empty dict should be None
30+
# Empty dict should remain empty dict in most cases
31+
return {}
2932

3033
# Process dictionary recursively
3134
fixed = {}
@@ -47,7 +50,7 @@ def fix_r_json(obj):
4750
fixed[k] = []
4851
elif k == "content" and isinstance(v, list):
4952
# content is already an array, just fix nested arrays
50-
fixed[k] = [fix_r_json(item) for item in v]
53+
fixed[k] = [fix_r_json(item, k) for item in v]
5154
elif k == "error" and isinstance(v, dict):
5255
# Process error object specially
5356
error_fixed = {}
@@ -58,8 +61,8 @@ def fix_r_json(obj):
5861
error_fixed[ek] = ev
5962
fixed[k] = error_fixed
6063
else:
61-
# Recursive fix
62-
fixed[k] = fix_r_json(v)
64+
# Recursive fix with key context
65+
fixed[k] = fix_r_json(v, k)
6366
return fixed
6467
else:
6568
return obj

0 commit comments

Comments
 (0)