Skip to content

Commit 95b096e

Browse files
committed
Refactor and simplify JSON conversion functions for better clarity
- Rename functions for better clarity: * _auto_convert_json_args → _wrap_with_json_conversion * _modify_schema_for_json_string_support → _update_schema_for_json_args - Create shared helper _is_dict_compatible_annotation() to consolidate duplicate union type detection logic across functions - Simplify union type handling with unified approach for both typing.Union and Python 3.10+ UnionType syntax - Rename internal helper _modify_annotation_for_string_support to _add_string_to_annotation for clarity - Reduce code duplication by ~15 lines while maintaining all functionality - All 44 tests still pass, including 11 JSON conversion tests
1 parent eb88f88 commit 95b096e

File tree

2 files changed

+50
-69
lines changed

2 files changed

+50
-69
lines changed

jupyter_server_mcp/mcp_server.py

Lines changed: 39 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,23 @@
1515
logger = logging.getLogger(__name__)
1616

1717

18-
def _auto_convert_json_args(func: Callable) -> Callable:
18+
def _is_dict_compatible_annotation(annotation) -> bool:
19+
"""Check if an annotation expects dict values that can be JSON-converted."""
20+
# Direct dict annotation
21+
if annotation is dict:
22+
return True
23+
24+
# Union types: Optional[dict], Union[dict, None], dict | None
25+
origin = get_origin(annotation)
26+
if origin is Union or (hasattr(annotation, '__class__') and annotation.__class__.__name__ == 'UnionType'):
27+
args = get_args(annotation)
28+
return dict in args
29+
30+
# Typed dict annotations: Dict[K, V], dict[str, Any]
31+
return bool(hasattr(annotation, '__origin__') and annotation.__origin__ is dict)
32+
33+
34+
def _wrap_with_json_conversion(func: Callable) -> Callable:
1935
"""
2036
Wrapper that automatically converts JSON string arguments to dictionaries.
2137
@@ -40,60 +56,36 @@ def _auto_convert_json_args(func: Callable) -> Callable:
4056

4157
def _should_convert_to_dict(annotation, value):
4258
"""Check if a parameter should be converted from JSON string to dict."""
43-
if not isinstance(value, str):
44-
return False
45-
46-
# Direct dict annotation
47-
if annotation is dict:
48-
return True
49-
50-
# Optional[dict] or Union[dict, None] etc. (old typing.Union)
51-
origin = get_origin(annotation)
52-
if origin is Union:
53-
args = get_args(annotation)
54-
return dict in args
55-
56-
# New Python 3.10+ union syntax: dict | None
57-
if hasattr(annotation, '__class__') and annotation.__class__.__name__ == 'UnionType':
58-
# For dict | None style unions
59-
args = get_args(annotation)
60-
return dict in args
61-
62-
# Dict[K, V] style annotations
63-
return bool(hasattr(annotation, '__origin__') and annotation.__origin__ is dict)
59+
return isinstance(value, str) and _is_dict_compatible_annotation(annotation)
6460

65-
def _modify_annotation_for_string_support(annotation):
61+
def _add_string_to_annotation(annotation):
6662
"""Modify annotation to also accept strings for dict types."""
6763
# Direct dict annotation -> dict | str
6864
if annotation is dict:
6965
return dict | str
7066

71-
# Optional[dict] or Union[dict, None] etc. (old typing.Union)
67+
# Union types: add str to existing union
7268
origin = get_origin(annotation)
7369
if origin is Union:
7470
args = get_args(annotation)
75-
if dict in args:
76-
# Add str to the union if it's not already there
77-
if str not in args:
78-
return Union[(*tuple(args), str)]
79-
return annotation
71+
if dict in args and str not in args:
72+
return Union[(*tuple(args), str)]
73+
return annotation
8074

8175
# New Python 3.10+ union syntax: dict | None
8276
if hasattr(annotation, '__class__') and annotation.__class__.__name__ == 'UnionType':
8377
args = get_args(annotation)
84-
if dict in args:
85-
# Add str to the union if it's not already there
86-
if str not in args:
87-
# Reconstruct the union with str added
88-
new_args = (*tuple(args), str)
89-
# Create new union type
90-
result = new_args[0]
91-
for arg in new_args[1:]:
92-
result = result | arg
93-
return result
94-
return annotation
78+
if dict in args and str not in args:
79+
# Reconstruct the union with str added
80+
new_args = (*tuple(args), str)
81+
# Create new union type
82+
result = new_args[0]
83+
for arg in new_args[1:]:
84+
result = result | arg
85+
return result
86+
return annotation
9587

96-
# Dict[K, V] style annotations -> annotation | str
88+
# Typed dict annotations -> annotation | str
9789
if hasattr(annotation, '__origin__') and annotation.__origin__ is dict:
9890
return annotation | str
9991

@@ -103,7 +95,7 @@ def _modify_annotation_for_string_support(annotation):
10395
new_annotations = {}
10496
for param_name, param in sig.parameters.items():
10597
if param.annotation != inspect.Parameter.empty:
106-
new_annotations[param_name] = _modify_annotation_for_string_support(param.annotation)
98+
new_annotations[param_name] = _add_string_to_annotation(param.annotation)
10799
else:
108100
new_annotations[param_name] = param.annotation
109101

@@ -136,6 +128,7 @@ async def async_wrapper(*args, **kwargs):
136128
# Set the modified annotations on the wrapper
137129
async_wrapper.__annotations__ = new_annotations
138130
return async_wrapper
131+
139132
@wraps(func)
140133
def sync_wrapper(*args, **kwargs):
141134
# Convert keyword arguments that should be dicts but are strings
@@ -162,7 +155,7 @@ def sync_wrapper(*args, **kwargs):
162155
return sync_wrapper
163156

164157

165-
def _modify_schema_for_json_string_support(func: Callable, tool) -> None:
158+
def _update_schema_for_json_args(func: Callable, tool) -> None:
166159
"""
167160
Modify the tool's JSON schema to accept strings for dict parameters.
168161
@@ -191,19 +184,7 @@ def _modify_schema_for_json_string_support(func: Callable, tool) -> None:
191184

192185
# Check if this parameter should support JSON string conversion
193186
annotation = param.annotation
194-
should_support_string = False
195-
196-
# Direct dict annotation
197-
if annotation is dict:
198-
should_support_string = True
199-
# Optional[dict] or Union[dict, None] etc. (old typing.Union)
200-
elif get_origin(annotation) is Union or (hasattr(annotation, '__class__') and annotation.__class__.__name__ == 'UnionType'):
201-
args = get_args(annotation)
202-
if dict in args:
203-
should_support_string = True
204-
# Dict[K, V] style annotations
205-
elif hasattr(annotation, '__origin__') and annotation.__origin__ is dict:
206-
should_support_string = True
187+
should_support_string = _is_dict_compatible_annotation(annotation)
207188

208189
if should_support_string:
209190
# Modify the schema to also accept strings
@@ -292,15 +273,15 @@ def register_tool(
292273
)
293274

294275
# Apply auto-conversion wrapper (always enabled)
295-
registered_func = _auto_convert_json_args(func)
276+
registered_func = _wrap_with_json_conversion(func)
296277
self.log.debug(f"Applied JSON argument auto-conversion wrapper to {tool_name}")
297278

298279
# Register with FastMCP
299280
tool = self.mcp.tool(registered_func)
300281

301282
# Modify schema to support JSON strings for dict parameters
302283
if tool:
303-
_modify_schema_for_json_string_support(func, tool)
284+
_update_schema_for_json_args(func, tool)
304285
self.log.debug(f"Modified schema for tool '{tool_name}' to accept JSON strings for dict parameters")
305286

306287
# Keep track for listing

tests/test_mcp_server.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import pytest
66

7-
from jupyter_server_mcp.mcp_server import MCPServer, _auto_convert_json_args
7+
from jupyter_server_mcp.mcp_server import MCPServer, _wrap_with_json_conversion
88

99

1010
def simple_function(x: int, y: int) -> int:
@@ -224,7 +224,7 @@ def func_with_dict(data: dict) -> dict:
224224
"""Function that expects a dict."""
225225
return {"received": data, "type": type(data).__name__}
226226

227-
wrapped_func = _auto_convert_json_args(func_with_dict)
227+
wrapped_func = _wrap_with_json_conversion(func_with_dict)
228228

229229
# Test with actual dict (should pass through)
230230
result = wrapped_func(data={"key": "value"})
@@ -243,7 +243,7 @@ def func_with_optional_dict(data: dict | None = None) -> dict:
243243
"""Function with optional dict parameter."""
244244
return {"received": data, "type": type(data).__name__ if data else "NoneType"}
245245

246-
wrapped_func = _auto_convert_json_args(func_with_optional_dict)
246+
wrapped_func = _wrap_with_json_conversion(func_with_optional_dict)
247247

248248
# Test with None (should pass through)
249249
result = wrapped_func(data=None)
@@ -262,7 +262,7 @@ def func_with_union_dict(data: dict | None) -> dict:
262262
"""Function with Union[dict, None] parameter."""
263263
return {"received": data, "type": type(data).__name__ if data else "NoneType"}
264264

265-
wrapped_func = _auto_convert_json_args(func_with_union_dict)
265+
wrapped_func = _wrap_with_json_conversion(func_with_union_dict)
266266

267267
# Test with JSON string (should be converted)
268268
result = wrapped_func(data='{"union": "test"}')
@@ -276,7 +276,7 @@ def func_with_typed_dict(config: dict[str, str]) -> dict:
276276
"""Function with Dict[str, str] annotation."""
277277
return {"received": config, "type": type(config).__name__}
278278

279-
wrapped_func = _auto_convert_json_args(func_with_typed_dict)
279+
wrapped_func = _wrap_with_json_conversion(func_with_typed_dict)
280280

281281
# Test with JSON string (should be converted)
282282
result = wrapped_func(config='{"name": "test", "value": "data"}')
@@ -290,7 +290,7 @@ def func_with_dict(data: dict) -> dict:
290290
"""Function that expects a dict."""
291291
return {"received": data, "type": type(data).__name__}
292292

293-
wrapped_func = _auto_convert_json_args(func_with_dict)
293+
wrapped_func = _wrap_with_json_conversion(func_with_dict)
294294

295295
# Test with invalid JSON (should pass string as-is)
296296
result = wrapped_func(data="invalid json {")
@@ -316,7 +316,7 @@ def mixed_func(name: str, count: int, data: dict) -> dict:
316316
"data_type": type(data).__name__
317317
}
318318

319-
wrapped_func = _auto_convert_json_args(mixed_func)
319+
wrapped_func = _wrap_with_json_conversion(mixed_func)
320320

321321
# Only the dict parameter should be converted
322322
result = wrapped_func(
@@ -341,7 +341,7 @@ async def async_func_with_dict(config: dict) -> dict:
341341
await asyncio.sleep(0.001) # Small delay
342342
return {"async_result": config, "type": type(config).__name__}
343343

344-
wrapped_func = _auto_convert_json_args(async_func_with_dict)
344+
wrapped_func = _wrap_with_json_conversion(async_func_with_dict)
345345

346346
# Test with JSON string (should be converted)
347347
result = await wrapped_func(config='{"async": true, "value": 123}')
@@ -355,7 +355,7 @@ def func_with_nested_dict(data: dict) -> dict:
355355
"""Function that processes nested dict data."""
356356
return {"processed": data}
357357

358-
wrapped_func = _auto_convert_json_args(func_with_nested_dict)
358+
wrapped_func = _wrap_with_json_conversion(func_with_nested_dict)
359359

360360
complex_json = '''{
361361
"users": [
@@ -388,7 +388,7 @@ def original_func(data: dict) -> dict:
388388
"""Original function with dict annotation."""
389389
return data
390390

391-
wrapped_func = _auto_convert_json_args(original_func)
391+
wrapped_func = _wrap_with_json_conversion(original_func)
392392

393393
# Check that annotations were modified to accept strings
394394
annotations = wrapped_func.__annotations__
@@ -435,7 +435,7 @@ def func_multiple_dicts(config: dict, metadata: dict, name: str) -> dict:
435435
}
436436
}
437437

438-
wrapped_func = _auto_convert_json_args(func_multiple_dicts)
438+
wrapped_func = _wrap_with_json_conversion(func_multiple_dicts)
439439

440440
result = wrapped_func(
441441
config='{"key1": "value1"}',

0 commit comments

Comments
 (0)