Skip to content

Commit cf88551

Browse files
fix(mcp): Allow MCP tools to accept string or object request formats (apache#36271)
1 parent aca18ff commit cf88551

File tree

2 files changed

+23
-9
lines changed

2 files changed

+23
-9
lines changed

superset/mcp_service/utils/schema_utils.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -390,8 +390,8 @@ def parse_request(
390390
Decorator to handle Claude Code bug where requests are double-serialized as strings.
391391
392392
Automatically parses string requests to Pydantic models before calling
393-
the tool function.
394-
This eliminates the need for manual parsing code in every tool function.
393+
the tool function. Also modifies the function's type annotations to accept
394+
str | RequestModel to pass FastMCP validation.
395395
396396
See: https://github.com/anthropics/claude-code/issues/5504
397397
@@ -406,15 +406,17 @@ def parse_request(
406406
@mcp_auth_hook
407407
@parse_request(ListChartsRequest)
408408
async def list_charts(
409-
request: ListChartsRequest, ctx: Context
409+
request: ListChartsRequest, ctx: Context # Keep clean type hint
410410
) -> ChartList:
411-
# Decorator handles string conversion automatically
411+
# Decorator handles string conversion and type annotation
412412
await ctx.info(f"Listing charts: page={request.page}")
413413
...
414414
415415
Note:
416416
- Works with both async and sync functions
417417
- Request must be the first positional argument
418+
- Modifies __annotations__ to accept str | RequestModel for FastMCP
419+
- Function implementation can use clean RequestModel type hint
418420
- If request is already a model instance, it passes through unchanged
419421
- Handles JSON string parsing with helpful error messages
420422
"""
@@ -429,7 +431,7 @@ async def async_wrapper(request: Any, *args: Any, **kwargs: Any) -> Any:
429431
parsed_request = parse_json_or_model(request, request_class, "request")
430432
return await func(parsed_request, *args, **kwargs)
431433

432-
return async_wrapper
434+
wrapper = async_wrapper
433435
else:
434436

435437
@wraps(func)
@@ -439,6 +441,15 @@ def sync_wrapper(request: Any, *args: Any, **kwargs: Any) -> Any:
439441
parsed_request = parse_json_or_model(request, request_class, "request")
440442
return func(parsed_request, *args, **kwargs)
441443

442-
return sync_wrapper
444+
wrapper = sync_wrapper
445+
446+
# Modify the wrapper's annotations to accept str | RequestModel
447+
# This allows FastMCP to accept string inputs while keeping the
448+
# original function's type hints clean
449+
if hasattr(wrapper, "__annotations__"):
450+
# Create union type: str | RequestModel
451+
wrapper.__annotations__["request"] = str | request_class
452+
453+
return wrapper
443454

444455
return decorator

tests/unit_tests/mcp_service/dashboard/tool/test_dashboard_tools.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,9 +191,12 @@ async def test_list_dashboards_with_string_filters(mock_list, mcp_server):
191191
async with Client(mcp_server) as client: # noqa: F841
192192
filters = '[{"col": "dashboard_title", "opr": "sw", "value": "Sales"}]'
193193

194-
# Test that string filters cause validation error at schema level
195-
with pytest.raises(ValueError, match="validation error"):
196-
ListDashboardsRequest(filters=filters) # noqa: F841
194+
# Test that string filters are now properly parsed to objects
195+
request = ListDashboardsRequest(filters=filters)
196+
assert len(request.filters) == 1
197+
assert request.filters[0].col == "dashboard_title"
198+
assert request.filters[0].opr == "sw"
199+
assert request.filters[0].value == "Sales"
197200

198201

199202
@patch("superset.daos.dashboard.DashboardDAO.list")

0 commit comments

Comments
 (0)