Coverage for mcpgateway / utils / base_models.py: 100%

8 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-02-11 07:10 +0000

1# -*- coding: utf-8 -*- 

2"""Base model utilities for MCP Gateway. 

3 

4This module provides shared base classes and utilities for Pydantic models 

5to avoid circular dependencies between models.py and schemas.py. 

6 

7Copyright 2025 

8SPDX-License-Identifier: Apache-2.0 

9""" 

10 

11# Standard 

12from typing import Any, Dict 

13 

14# Third-Party 

15from pydantic import BaseModel, ConfigDict 

16 

17 

18def to_camel_case(s: str) -> str: 

19 """Convert a string from snake_case to camelCase. 

20 

21 Args: 

22 s (str): The string to be converted, which is assumed to be in snake_case. 

23 

24 Returns: 

25 str: The string converted to camelCase. 

26 

27 Examples: 

28 >>> to_camel_case("hello_world_example") 

29 'helloWorldExample' 

30 >>> to_camel_case("alreadyCamel") 

31 'alreadyCamel' 

32 >>> to_camel_case("") 

33 '' 

34 >>> to_camel_case("single") 

35 'single' 

36 >>> to_camel_case("_leading_underscore") 

37 'LeadingUnderscore' 

38 >>> to_camel_case("trailing_underscore_") 

39 'trailingUnderscore' 

40 >>> to_camel_case("multiple_words_here") 

41 'multipleWordsHere' 

42 >>> to_camel_case("api_key_value") 

43 'apiKeyValue' 

44 >>> to_camel_case("user_id") 

45 'userId' 

46 >>> to_camel_case("created_at") 

47 'createdAt' 

48 """ 

49 return "".join(word.capitalize() if i else word for i, word in enumerate(s.split("_"))) 

50 

51 

52class BaseModelWithConfigDict(BaseModel): 

53 """Base model with common configuration for MCP protocol types. 

54 

55 This base class provides automatic snake_case → camelCase field name conversion 

56 to comply with the MCP specification's JSON naming conventions. 

57 

58 Key Features: 

59 - **Automatic camelCase conversion**: Field names like `stop_reason` automatically 

60 serialize as `stopReason` when FastAPI returns the response (via jsonable_encoder). 

61 - **ORM mode**: Can be constructed from SQLAlchemy models (from_attributes=True). 

62 - **Flexible input**: Accepts both snake_case and camelCase in input (populate_by_name=True). 

63 - **Enum values**: Enums serialize as their values, not names (use_enum_values=True). 

64 

65 Usage: 

66 Models extending this class will automatically serialize field names to camelCase: 

67 

68 >>> class MyModel(BaseModelWithConfigDict): 

69 ... my_field: str = "value" 

70 ... another_field: int = 42 

71 >>> 

72 >>> obj = MyModel() 

73 >>> obj.model_dump(by_alias=True) 

74 {'myField': 'value', 'anotherField': 42} 

75 

76 Important: 

77 FastAPI's default response serialization uses `by_alias=True`, so models extending 

78 this class will automatically use camelCase in JSON responses without any additional 

79 code changes. This is critical for MCP spec compliance. 

80 

81 Examples: 

82 >>> from mcpgateway.utils.base_models import BaseModelWithConfigDict 

83 >>> class CreateMessageResult(BaseModelWithConfigDict): 

84 ... stop_reason: str = "endTurn" 

85 >>> 

86 >>> result = CreateMessageResult() 

87 >>> # Without by_alias (internal Python usage): 

88 >>> result.model_dump() 

89 {'stop_reason': 'endTurn'} 

90 >>> 

91 >>> # With by_alias (FastAPI automatic serialization): 

92 >>> result.model_dump(by_alias=True) 

93 {'stopReason': 'endTurn'} 

94 """ 

95 

96 model_config = ConfigDict( 

97 from_attributes=True, 

98 alias_generator=to_camel_case, # Automatic snake_case → camelCase conversion 

99 populate_by_name=True, 

100 use_enum_values=True, 

101 extra="ignore", 

102 json_schema_extra={"nullable": True}, 

103 ) 

104 

105 def to_dict(self, use_alias: bool = False) -> Dict[str, Any]: 

106 """Convert the model instance into a dictionary representation. 

107 

108 Args: 

109 use_alias (bool): Whether to use aliases for field names (default is False). 

110 If True, field names will be converted using the alias generator. 

111 

112 Returns: 

113 Dict[str, Any]: A dictionary where keys are field names and values are 

114 corresponding field values, with any nested models recursively 

115 converted to dictionaries. 

116 """ 

117 return self.model_dump(by_alias=use_alias)