Coverage for mcp_chatkit_widget / tooling.py: 100%

33 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-11 06:37 +0000

1"""Tooling helpers that expose FastMCP registration utilities.""" 

2 

3from __future__ import annotations 

4import inspect 

5from collections.abc import Callable 

6from typing import Any, cast 

7from chatkit.widgets import WidgetComponentBase 

8from fastmcp import FastMCP 

9from mcp_chatkit_widget.naming import _sanitize_tool_name, _to_camel_case 

10from mcp_chatkit_widget.rendering import build_widget_model, render_widget_definition 

11from mcp_chatkit_widget.widget_loader import WidgetDefinition 

12 

13 

14def _create_widget_tool_function( 

15 widget_def: WidgetDefinition, 

16) -> Callable[..., WidgetComponentBase]: 

17 """Create a FastMCP tool function for a given widget definition.""" 

18 pydantic_model = build_widget_model(widget_def) 

19 

20 def widget_tool(**kwargs: Any) -> WidgetComponentBase: 

21 return render_widget_definition(widget_def, **kwargs) 

22 

23 camel_name = _to_camel_case(_sanitize_tool_name(widget_def.name)) 

24 widget_tool.__name__ = camel_name 

25 widget_tool.__doc__ = ( 

26 f"Generate a {widget_def.name} widget.\n\n" 

27 f"This tool creates a {widget_def.name} widget with the provided data.\n" 

28 f"The input must conform to the widget's JSON schema." 

29 ) 

30 

31 parameters: list[inspect.Parameter] = [] 

32 for field_name, field_info in pydantic_model.model_fields.items(): 

33 if field_info.is_required(): 

34 default = inspect.Parameter.empty 

35 else: 

36 default = field_info.default 

37 

38 parameters.append( 

39 inspect.Parameter( 

40 field_name, 

41 inspect.Parameter.KEYWORD_ONLY, 

42 default=default, 

43 annotation=field_info.annotation, 

44 ) 

45 ) 

46 

47 cast( 

48 Any, 

49 widget_tool, 

50 ).__signature__ = inspect.Signature( 

51 parameters, return_annotation=WidgetComponentBase 

52 ) 

53 

54 annotations: dict[str, Any] = { 

55 field_name: field_info.annotation 

56 for field_name, field_info in pydantic_model.model_fields.items() 

57 } 

58 annotations["return"] = WidgetComponentBase 

59 cast(Any, widget_tool).__annotations__ = annotations 

60 

61 return widget_tool 

62 

63 

64def generate_widget_tools(server: FastMCP, widget_defs: list[WidgetDefinition]) -> None: 

65 """Register sanitized FastMCP tools for each widget definition.""" 

66 for widget_def in widget_defs: 

67 tool_func = _create_widget_tool_function(widget_def) 

68 tool_name = _sanitize_tool_name(widget_def.name) 

69 server.tool(name=tool_name)(tool_func) 

70 

71 

72__all__ = [ 

73 "_sanitize_tool_name", 

74 "_to_camel_case", 

75 "_create_widget_tool_function", 

76 "generate_widget_tools", 

77]