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
« 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"""
5from typing import Any, Dict, List, Optional, Callable
6from pathlib import Path
7import logging
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
14logger = logging.getLogger(__name__)
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.
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)
34 Returns:
35 QuickMCPServer instance
37 Raises:
38 MissingDependencyError: If required dependencies are missing
40 Example:
41 ```python
42 # Create MCP server from a Python file
43 server = create_mcp_from_module("my_utils.py")
45 # Or from an installed module
46 server = create_mcp_from_module("numpy", server_name="numpy-mcp")
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)
56 if auto_run:
57 server.run()
59 return server
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.
66 Args:
67 obj: Python object (module, class, instance, or dict of functions)
68 server_name: Optional server name
69 config: Factory configuration
71 Returns:
72 QuickMCPServer instance
73 """
74 import inspect
76 factory = MCPFactory(name=server_name, config=config)
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")
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.
99 Args:
100 file_path: Path to Python file
101 config: Factory configuration
103 Returns:
104 List of missing dependencies
105 """
106 factory = MCPFactory(config=config)
107 return factory.analyze_dependencies(file_path)
110def check_dependencies(file_path: str, config: Optional[FactoryConfig] = None) -> Dict[str, Any]:
111 """
112 Check dependencies and return a detailed report.
114 Args:
115 file_path: Path to Python file
116 config: Factory configuration
118 Returns:
119 Dictionary with dependency analysis results
120 """
121 factory = MCPFactory(config=config)
122 return factory.check_dependencies(file_path)
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.
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)
137 print(f"\nDependency Analysis for: {file_path}")
138 print("=" * 60)
140 if report["total_missing"] == 0:
141 print("✅ All dependencies are available!")
142 return
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'}")
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}")
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}")
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}")
184 if report["install_command"]:
185 print(f"\n💡 Quick install command (required):")
186 print(f" {report['install_command']}")
188 print()
190 except Exception as e:
191 error_msg = handle_factory_error(e, "Dependency analysis")
192 print(f"Error: {error_msg}")
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.
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
205 Returns:
206 Dictionary with install commands for different dependency types
207 """
208 try:
209 report = check_dependencies(file_path, config)
210 commands = {}
212 # Required dependencies
213 if report["install_command"]:
214 commands["required"] = report["install_command"]
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)}"
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)}"
228 return commands
230 except Exception as e:
231 logger.error(f"Failed to get install commands: {e}")
232 return {}
235def validate_factory_setup(file_path: str, config: Optional[FactoryConfig] = None) -> bool:
236 """
237 Validate that a file can be used with the factory.
239 Args:
240 file_path: Path to Python file
241 config: Factory configuration
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
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.
259 Args:
260 func: Function to decorate
261 name: Optional tool name (defaults to function name)
262 description: Optional description (defaults to docstring)
264 Example:
265 ```python
266 @mcp_tool
267 def calculate(x: int, y: int) -> int:
268 return x + y
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
281 if func is None:
282 return decorator
283 else:
284 return decorator(func)
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)
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)
300def create_factory_with_config(**kwargs) -> MCPFactory:
301 """Create a factory with custom configuration options."""
302 config = FactoryConfig(**kwargs)
303 return MCPFactory(config=config)