Coverage for src/quickmcp/factory/utils.py: 44%

123 statements  

« prev     ^ index     » next       coverage.py v7.10.4, created at 2025-08-20 20:56 +0200

1""" 

2Utility functions and convenience functions for MCP Factory. 

3""" 

4 

5from typing import Any, Dict, List, Optional, Callable 

6from pathlib import Path 

7import logging 

8 

9from .core import MCPFactory 

10from .config import FactoryConfig, DEFAULT_CONFIG, create_safe_config, create_development_config 

11from .import_analyzer import MissingDependency 

12from .errors import MissingDependencyError, handle_factory_error 

13 

14logger = logging.getLogger(__name__) 

15 

16 

17def create_mcp_from_module( 

18 module_path: str, 

19 server_name: Optional[str] = None, 

20 include_private: bool = False, 

21 auto_run: bool = True, 

22 config: Optional[FactoryConfig] = None 

23): 

24 """ 

25 Convenience function to create and optionally run an MCP server from a module. 

26  

27 Args: 

28 module_path: Path to Python file or module name 

29 server_name: Optional server name 

30 include_private: Include private functions 

31 auto_run: Automatically run the server 

32 config: Factory configuration (defaults to DEFAULT_CONFIG) 

33  

34 Returns: 

35 QuickMCPServer instance 

36  

37 Raises: 

38 MissingDependencyError: If required dependencies are missing 

39  

40 Example: 

41 ```python 

42 # Create MCP server from a Python file 

43 server = create_mcp_from_module("my_utils.py") 

44  

45 # Or from an installed module 

46 server = create_mcp_from_module("numpy", server_name="numpy-mcp") 

47  

48 # With custom configuration 

49 config = create_safe_config() 

50 server = create_mcp_from_module("my_utils.py", config=config) 

51 ``` 

52 """ 

53 factory = MCPFactory(name=server_name, config=config) 

54 server = factory.from_module(module_path, include_private=include_private) 

55 

56 if auto_run: 

57 server.run() 

58 

59 return server 

60 

61 

62def create_mcp_from_object(obj: Any, server_name: Optional[str] = None, config: Optional[FactoryConfig] = None): 

63 """ 

64 Create an MCP server from any Python object. 

65  

66 Args: 

67 obj: Python object (module, class, instance, or dict of functions) 

68 server_name: Optional server name 

69 config: Factory configuration 

70  

71 Returns: 

72 QuickMCPServer instance 

73 """ 

74 import inspect 

75 

76 factory = MCPFactory(name=server_name, config=config) 

77 

78 if isinstance(obj, dict): 

79 # Dictionary of functions 

80 return factory.from_functions(obj, name=server_name) 

81 elif inspect.isclass(obj): 

82 # Class 

83 return factory.from_class(obj) 

84 elif inspect.ismodule(obj): 

85 # Module - use from_module_object for already-loaded modules 

86 return factory.from_module_object(obj) 

87 elif hasattr(obj, '__class__') and obj.__class__.__module__ != 'builtins': 

88 # Instance of a custom class (not built-in types) 

89 return factory.from_class(obj.__class__) 

90 else: 

91 raise ValueError(f"Cannot create MCP server from {type(obj).__name__} object") 

92 

93 

94# Standalone utility functions for dependency analysis 

95def analyze_dependencies(file_path: str, config: Optional[FactoryConfig] = None) -> List[MissingDependency]: 

96 """ 

97 Analyze dependencies of a Python file. 

98  

99 Args: 

100 file_path: Path to Python file 

101 config: Factory configuration 

102  

103 Returns: 

104 List of missing dependencies 

105 """ 

106 factory = MCPFactory(config=config) 

107 return factory.analyze_dependencies(file_path) 

108 

109 

110def check_dependencies(file_path: str, config: Optional[FactoryConfig] = None) -> Dict[str, Any]: 

111 """ 

112 Check dependencies and return a detailed report. 

113  

114 Args: 

115 file_path: Path to Python file 

116 config: Factory configuration 

117  

118 Returns: 

119 Dictionary with dependency analysis results 

120 """ 

121 factory = MCPFactory(config=config) 

122 return factory.check_dependencies(file_path) 

123 

124 

125def print_dependency_report(file_path: str, config: Optional[FactoryConfig] = None, include_dev: bool = True) -> None: 

126 """ 

127 Print a formatted dependency report for a Python file. 

128  

129 Args: 

130 file_path: Path to Python file 

131 config: Factory configuration 

132 include_dev: Include development dependencies in report 

133 """ 

134 try: 

135 report = check_dependencies(file_path, config) 

136 

137 print(f"\nDependency Analysis for: {file_path}") 

138 print("=" * 60) 

139 

140 if report["total_missing"] == 0: 

141 print("✅ All dependencies are available!") 

142 return 

143 

144 print(f"Total missing: {report['total_missing']}") 

145 print(f"Required missing: {report['required_missing']}") 

146 print(f"Optional missing: {report['optional_missing']}") 

147 if include_dev: 

148 print(f"Dev missing: {report['dev_missing']}") 

149 print(f"Can load module: {'✅ Yes' if report['can_load'] else '❌ No'}") 

150 

151 if report["required_dependencies"]: 

152 print("\n❌ Required dependencies (will cause import errors):") 

153 for dep in report["required_dependencies"]: 

154 print(f" • {dep.module}") 

155 if dep.source_line and dep.line_number: 

156 print(f" Line {dep.line_number}: {dep.source_line}") 

157 install_pkg = dep.suggested_install or dep.module 

158 if install_pkg != dep.module: 

159 print(f" Install: pip install {install_pkg}") 

160 else: 

161 print(f" Install: pip install {dep.module}") 

162 

163 if report["optional_dependencies"]: 

164 print("\n⚠️ Optional dependencies (handled gracefully):") 

165 for dep in report["optional_dependencies"]: 

166 if not dep.is_dev_dependency: # Don't show dev deps in optional section 

167 print(f" • {dep.module}") 

168 install_pkg = dep.suggested_install or dep.module 

169 if install_pkg != dep.module: 

170 print(f" Install: pip install {install_pkg}") 

171 else: 

172 print(f" Install: pip install {dep.module}") 

173 

174 if include_dev and report["dev_dependencies"]: 

175 print("\n🛠️ Development dependencies:") 

176 for dep in report["dev_dependencies"]: 

177 print(f" • {dep.module}") 

178 install_pkg = dep.suggested_install or dep.module 

179 if install_pkg != dep.module: 

180 print(f" Install: pip install {install_pkg}") 

181 else: 

182 print(f" Install: pip install {dep.module}") 

183 

184 if report["install_command"]: 

185 print(f"\n💡 Quick install command (required):") 

186 print(f" {report['install_command']}") 

187 

188 print() 

189 

190 except Exception as e: 

191 error_msg = handle_factory_error(e, "Dependency analysis") 

192 print(f"Error: {error_msg}") 

193 

194 

195def get_install_command(file_path: str, include_optional: bool = False, include_dev: bool = False, config: Optional[FactoryConfig] = None) -> Dict[str, Optional[str]]: 

196 """ 

197 Get pip install commands for missing dependencies. 

198  

199 Args: 

200 file_path: Path to Python file 

201 include_optional: Include optional dependencies 

202 include_dev: Include development dependencies 

203 config: Factory configuration 

204  

205 Returns: 

206 Dictionary with install commands for different dependency types 

207 """ 

208 try: 

209 report = check_dependencies(file_path, config) 

210 commands = {} 

211 

212 # Required dependencies 

213 if report["install_command"]: 

214 commands["required"] = report["install_command"] 

215 

216 # Optional dependencies 

217 if include_optional and report["optional_dependencies"]: 

218 optional_non_dev = [dep for dep in report["optional_dependencies"] if not dep.is_dev_dependency] 

219 if optional_non_dev: 

220 packages = set(dep.suggested_install or dep.module for dep in optional_non_dev) 

221 commands["optional"] = f"pip install {' '.join(packages)}" 

222 

223 # Development dependencies 

224 if include_dev and report["dev_dependencies"]: 

225 packages = set(dep.suggested_install or dep.module for dep in report["dev_dependencies"]) 

226 commands["dev"] = f"pip install {' '.join(packages)}" 

227 

228 return commands 

229 

230 except Exception as e: 

231 logger.error(f"Failed to get install commands: {e}") 

232 return {} 

233 

234 

235def validate_factory_setup(file_path: str, config: Optional[FactoryConfig] = None) -> bool: 

236 """ 

237 Validate that a file can be used with the factory. 

238  

239 Args: 

240 file_path: Path to Python file 

241 config: Factory configuration 

242  

243 Returns: 

244 True if file can be loaded, False otherwise 

245 """ 

246 try: 

247 report = check_dependencies(file_path, config) 

248 return report["can_load"] 

249 except Exception as e: 

250 logger.error(f"Validation failed: {e}") 

251 return False 

252 

253 

254# Decorator for marking functions to be exposed as MCP tools 

255def mcp_tool(func: Optional[Callable] = None, *, name: Optional[str] = None, description: Optional[str] = None): 

256 """ 

257 Decorator to mark a function for MCP exposure. 

258  

259 Args: 

260 func: Function to decorate 

261 name: Optional tool name (defaults to function name) 

262 description: Optional description (defaults to docstring) 

263  

264 Example: 

265 ```python 

266 @mcp_tool 

267 def calculate(x: int, y: int) -> int: 

268 return x + y 

269  

270 @mcp_tool(name="custom_name", description="Custom description") 

271 async def my_async_function(): 

272 pass 

273 ``` 

274 """ 

275 def decorator(f): 

276 f._mcp_tool = True 

277 f._mcp_name = name or f.__name__ 

278 f._mcp_description = description or f.__doc__ 

279 return f 

280 

281 if func is None: 

282 return decorator 

283 else: 

284 return decorator(func) 

285 

286 

287# Configuration shortcuts 

288def create_factory_for_development(name: Optional[str] = None) -> MCPFactory: 

289 """Create a factory configured for development use.""" 

290 config = create_development_config() 

291 return MCPFactory(name=name, config=config) 

292 

293 

294def create_safe_factory(name: Optional[str] = None) -> MCPFactory: 

295 """Create a factory configured for safe operation.""" 

296 config = create_safe_config() 

297 return MCPFactory(name=name, config=config) 

298 

299 

300def create_factory_with_config(**kwargs) -> MCPFactory: 

301 """Create a factory with custom configuration options.""" 

302 config = FactoryConfig(**kwargs) 

303 return MCPFactory(config=config)