mseep-lightfast-mcp 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. common/__init__.py +21 -0
  2. common/types.py +182 -0
  3. lightfast_mcp/__init__.py +50 -0
  4. lightfast_mcp/core/__init__.py +14 -0
  5. lightfast_mcp/core/base_server.py +205 -0
  6. lightfast_mcp/exceptions.py +55 -0
  7. lightfast_mcp/servers/__init__.py +1 -0
  8. lightfast_mcp/servers/blender/__init__.py +5 -0
  9. lightfast_mcp/servers/blender/server.py +358 -0
  10. lightfast_mcp/servers/blender_mcp_server.py +82 -0
  11. lightfast_mcp/servers/mock/__init__.py +5 -0
  12. lightfast_mcp/servers/mock/server.py +101 -0
  13. lightfast_mcp/servers/mock/tools.py +161 -0
  14. lightfast_mcp/servers/mock_server.py +78 -0
  15. lightfast_mcp/utils/__init__.py +1 -0
  16. lightfast_mcp/utils/logging_utils.py +69 -0
  17. mseep_lightfast_mcp-0.0.1.dist-info/METADATA +36 -0
  18. mseep_lightfast_mcp-0.0.1.dist-info/RECORD +43 -0
  19. mseep_lightfast_mcp-0.0.1.dist-info/WHEEL +5 -0
  20. mseep_lightfast_mcp-0.0.1.dist-info/entry_points.txt +7 -0
  21. mseep_lightfast_mcp-0.0.1.dist-info/licenses/LICENSE +21 -0
  22. mseep_lightfast_mcp-0.0.1.dist-info/top_level.txt +3 -0
  23. tools/__init__.py +46 -0
  24. tools/ai/__init__.py +8 -0
  25. tools/ai/conversation_cli.py +345 -0
  26. tools/ai/conversation_client.py +399 -0
  27. tools/ai/conversation_session.py +342 -0
  28. tools/ai/providers/__init__.py +11 -0
  29. tools/ai/providers/base_provider.py +64 -0
  30. tools/ai/providers/claude_provider.py +200 -0
  31. tools/ai/providers/openai_provider.py +204 -0
  32. tools/ai/tool_executor.py +257 -0
  33. tools/common/__init__.py +99 -0
  34. tools/common/async_utils.py +419 -0
  35. tools/common/errors.py +222 -0
  36. tools/common/logging.py +252 -0
  37. tools/common/types.py +130 -0
  38. tools/orchestration/__init__.py +15 -0
  39. tools/orchestration/cli.py +320 -0
  40. tools/orchestration/config_loader.py +348 -0
  41. tools/orchestration/server_orchestrator.py +466 -0
  42. tools/orchestration/server_registry.py +187 -0
  43. tools/orchestration/server_selector.py +242 -0
@@ -0,0 +1,252 @@
1
+ """Structured logging with correlation IDs and metrics."""
2
+
3
+ import json
4
+ import logging
5
+ import uuid
6
+ from contextvars import ContextVar
7
+ from datetime import datetime
8
+ from functools import wraps
9
+ from typing import Any, Callable, Dict, Optional
10
+
11
+ from rich.console import Console
12
+ from rich.logging import RichHandler
13
+
14
+ # Context variables for request tracing
15
+ correlation_id: ContextVar[str] = ContextVar("correlation_id", default="")
16
+ operation_context: ContextVar[Dict[str, Any]] = ContextVar(
17
+ "operation_context", default={}
18
+ )
19
+
20
+
21
+ class StructuredFormatter(logging.Formatter):
22
+ """JSON formatter for structured logging."""
23
+
24
+ def format(self, record: logging.LogRecord) -> str:
25
+ # Create base log entry
26
+ log_entry = {
27
+ "timestamp": datetime.utcnow().isoformat(),
28
+ "level": record.levelname,
29
+ "logger": record.name,
30
+ "message": record.getMessage(),
31
+ "module": record.module,
32
+ "function": record.funcName,
33
+ "line": record.lineno,
34
+ }
35
+
36
+ # Add correlation ID if available
37
+ if correlation_id.get():
38
+ log_entry["correlation_id"] = correlation_id.get()
39
+
40
+ # Add operation context if available
41
+ context = operation_context.get()
42
+ if context:
43
+ log_entry["context"] = context
44
+
45
+ # Add any extra fields from the log record
46
+ if hasattr(record, "extra_fields"):
47
+ log_entry.update(record.extra_fields)
48
+
49
+ # Add exception info if present
50
+ if record.exc_info:
51
+ log_entry["exception"] = self.formatException(record.exc_info)
52
+
53
+ return json.dumps(log_entry, default=str)
54
+
55
+
56
+ class StructuredLogger:
57
+ """Enhanced logger with structured output and correlation IDs."""
58
+
59
+ def __init__(self, name: str, use_rich: bool = True):
60
+ self.logger = logging.getLogger(f"LightfastMCP.{name}")
61
+ self.use_rich = use_rich
62
+ self._setup_formatter()
63
+
64
+ def _setup_formatter(self):
65
+ """Setup structured formatter or rich handler."""
66
+ # Remove existing handlers to avoid duplicates
67
+ for handler in self.logger.handlers[:]:
68
+ self.logger.removeHandler(handler)
69
+
70
+ if self.use_rich:
71
+ # Use Rich handler for development/interactive use
72
+ handler = RichHandler(
73
+ console=Console(stderr=True),
74
+ rich_tracebacks=True,
75
+ show_path=False,
76
+ show_time=True,
77
+ )
78
+ handler.setFormatter(logging.Formatter("%(message)s"))
79
+ else:
80
+ # Use JSON formatter for production/structured logging
81
+ handler = logging.StreamHandler()
82
+ handler.setFormatter(StructuredFormatter())
83
+
84
+ self.logger.addHandler(handler)
85
+ self.logger.setLevel(logging.INFO)
86
+ self.logger.propagate = False
87
+
88
+ def _log_with_context(self, level: int, message: str, **kwargs):
89
+ """Log with correlation ID and context."""
90
+ extra_fields = {
91
+ "correlation_id": correlation_id.get(),
92
+ "context": operation_context.get(),
93
+ **kwargs,
94
+ }
95
+
96
+ # Create a log record with extra fields
97
+ extra = {"extra_fields": extra_fields}
98
+ self.logger.log(level, message, extra=extra)
99
+
100
+ def debug(self, message: str, **kwargs):
101
+ """Log debug message with context."""
102
+ self._log_with_context(logging.DEBUG, message, **kwargs)
103
+
104
+ def info(self, message: str, **kwargs):
105
+ """Log info message with context."""
106
+ self._log_with_context(logging.INFO, message, **kwargs)
107
+
108
+ def warning(self, message: str, **kwargs):
109
+ """Log warning message with context."""
110
+ self._log_with_context(logging.WARNING, message, **kwargs)
111
+
112
+ def error(self, message: str, error: Optional[Exception] = None, **kwargs):
113
+ """Log error message with context and optional exception."""
114
+ if error:
115
+ kwargs["error_type"] = type(error).__name__
116
+ kwargs["error_details"] = str(error)
117
+ if hasattr(error, "error_code"):
118
+ kwargs["error_code"] = error.error_code
119
+ if hasattr(error, "details"):
120
+ kwargs["error_context"] = error.details
121
+
122
+ self._log_with_context(logging.ERROR, message, **kwargs)
123
+
124
+ # Also log the exception traceback if provided
125
+ if error:
126
+ self.logger.exception("Exception details:", exc_info=error)
127
+
128
+ def critical(self, message: str, error: Optional[Exception] = None, **kwargs):
129
+ """Log critical message with context."""
130
+ if error:
131
+ kwargs["error_type"] = type(error).__name__
132
+ kwargs["error_details"] = str(error)
133
+
134
+ self._log_with_context(logging.CRITICAL, message, **kwargs)
135
+
136
+
137
+ def get_logger(name: str, use_rich: bool = True) -> StructuredLogger:
138
+ """Get a structured logger instance.
139
+
140
+ Args:
141
+ name: Logger name (will be prefixed with 'LightfastMCP.')
142
+ use_rich: Whether to use Rich formatting (True) or JSON (False)
143
+ """
144
+ return StructuredLogger(name, use_rich=use_rich)
145
+
146
+
147
+ def with_correlation_id(func: Optional[Callable] = None, *, generate_new: bool = True):
148
+ """Decorator to add correlation ID to operations.
149
+
150
+ Args:
151
+ func: Function to decorate
152
+ generate_new: Whether to generate a new correlation ID if one doesn't exist
153
+ """
154
+
155
+ def decorator(f: Callable) -> Callable:
156
+ @wraps(f)
157
+ async def async_wrapper(*args, **kwargs):
158
+ if generate_new and not correlation_id.get():
159
+ correlation_id.set(str(uuid.uuid4()))
160
+ return await f(*args, **kwargs)
161
+
162
+ @wraps(f)
163
+ def sync_wrapper(*args, **kwargs):
164
+ if generate_new and not correlation_id.get():
165
+ correlation_id.set(str(uuid.uuid4()))
166
+ return f(*args, **kwargs)
167
+
168
+ # Return appropriate wrapper based on function type
169
+ import asyncio
170
+
171
+ if asyncio.iscoroutinefunction(f):
172
+ return async_wrapper
173
+ else:
174
+ return sync_wrapper
175
+
176
+ if func is None:
177
+ return decorator
178
+ else:
179
+ return decorator(func)
180
+
181
+
182
+ def with_operation_context(**context_kwargs):
183
+ """Decorator to add operation context to logs.
184
+
185
+ Args:
186
+ **context_kwargs: Context key-value pairs to add
187
+ """
188
+
189
+ def decorator(func: Callable) -> Callable:
190
+ @wraps(func)
191
+ async def async_wrapper(*args, **kwargs):
192
+ # Merge with existing context
193
+ current_context = operation_context.get()
194
+ new_context = {**current_context, **context_kwargs}
195
+ operation_context.set(new_context)
196
+ try:
197
+ return await func(*args, **kwargs)
198
+ finally:
199
+ # Restore previous context
200
+ operation_context.set(current_context)
201
+
202
+ @wraps(func)
203
+ def sync_wrapper(*args, **kwargs):
204
+ current_context = operation_context.get()
205
+ new_context = {**current_context, **context_kwargs}
206
+ operation_context.set(new_context)
207
+ try:
208
+ return func(*args, **kwargs)
209
+ finally:
210
+ operation_context.set(current_context)
211
+
212
+ import asyncio
213
+
214
+ if asyncio.iscoroutinefunction(func):
215
+ return async_wrapper
216
+ else:
217
+ return sync_wrapper
218
+
219
+ return decorator
220
+
221
+
222
+ def configure_logging(
223
+ level: str = "INFO", use_rich: bool = True, logger_name: str = "LightfastMCP"
224
+ ) -> None:
225
+ """Configure logging for the entire application.
226
+
227
+ Args:
228
+ level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
229
+ use_rich: Whether to use Rich formatting
230
+ logger_name: Root logger name
231
+ """
232
+ root_logger = logging.getLogger(logger_name)
233
+ root_logger.setLevel(getattr(logging, level.upper()))
234
+
235
+ # Remove existing handlers
236
+ for handler in root_logger.handlers[:]:
237
+ root_logger.removeHandler(handler)
238
+
239
+ if use_rich:
240
+ handler = RichHandler(
241
+ console=Console(stderr=True),
242
+ rich_tracebacks=True,
243
+ show_path=False,
244
+ show_time=True,
245
+ )
246
+ handler.setFormatter(logging.Formatter("%(message)s"))
247
+ else:
248
+ handler = logging.StreamHandler()
249
+ handler.setFormatter(StructuredFormatter())
250
+
251
+ root_logger.addHandler(handler)
252
+ root_logger.propagate = False
tools/common/types.py ADDED
@@ -0,0 +1,130 @@
1
+ """Common types and data structures for the tools package."""
2
+
3
+ # Import shared types from common module
4
+ import uuid
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime
7
+ from typing import Any, Dict, Generic, List, Optional, TypeVar
8
+
9
+ try:
10
+ from common import OperationStatus, ToolCall, ToolResult
11
+ except ImportError:
12
+ # Fallback for development/testing
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent))
17
+ from common import OperationStatus, ToolCall, ToolResult
18
+
19
+ T = TypeVar("T")
20
+
21
+
22
+ @dataclass
23
+ class Result(Generic[T]):
24
+ """Standard result type for all operations."""
25
+
26
+ status: OperationStatus
27
+ data: Optional[T] = None
28
+ error: Optional[str] = None
29
+ error_code: Optional[str] = None
30
+ timestamp: Optional[datetime] = None
31
+ duration_ms: Optional[float] = None
32
+ correlation_id: Optional[str] = None
33
+
34
+ def __post_init__(self):
35
+ if self.timestamp is None:
36
+ self.timestamp = datetime.utcnow()
37
+ if self.correlation_id is None:
38
+ self.correlation_id = str(uuid.uuid4())
39
+
40
+ @property
41
+ def is_success(self) -> bool:
42
+ return self.status == OperationStatus.SUCCESS
43
+
44
+ @property
45
+ def is_failed(self) -> bool:
46
+ return self.status == OperationStatus.FAILED
47
+
48
+ @property
49
+ def is_pending(self) -> bool:
50
+ return self.status == OperationStatus.PENDING
51
+
52
+
53
+ # ServerInfo, ToolCall, and ToolResult are now imported from common module
54
+
55
+
56
+ @dataclass
57
+ class ConversationStep:
58
+ """Represents a single step in a conversation."""
59
+
60
+ step_number: int
61
+ text: str = ""
62
+ tool_calls: List[ToolCall] = field(default_factory=list)
63
+ tool_results: List[ToolResult] = field(default_factory=list)
64
+ finish_reason: Optional[str] = None
65
+ usage: Optional[Dict[str, Any]] = None
66
+ timestamp: Optional[datetime] = None
67
+ duration_ms: Optional[float] = None
68
+
69
+ def __post_init__(self):
70
+ if self.timestamp is None:
71
+ self.timestamp = datetime.utcnow()
72
+
73
+ def add_tool_call(self, tool_call: ToolCall) -> None:
74
+ """Add a tool call to this step."""
75
+ self.tool_calls.append(tool_call)
76
+
77
+ def add_tool_result(self, tool_result: ToolResult) -> None:
78
+ """Add a tool result to this step."""
79
+ self.tool_results.append(tool_result)
80
+
81
+ def has_pending_tool_calls(self) -> bool:
82
+ """Check if this step has tool calls that haven't been executed yet."""
83
+ executed_ids = {result.id for result in self.tool_results}
84
+ return any(call.id not in executed_ids for call in self.tool_calls)
85
+
86
+ @property
87
+ def has_errors(self) -> bool:
88
+ """Check if any tool results have errors."""
89
+ return any(result.is_error for result in self.tool_results)
90
+
91
+
92
+ @dataclass
93
+ class ConversationResult:
94
+ """Result of a conversation interaction."""
95
+
96
+ session_id: str
97
+ steps: List[ConversationStep]
98
+ total_duration_ms: Optional[float] = None
99
+ total_tool_calls: int = 0
100
+ successful_tool_calls: int = 0
101
+ failed_tool_calls: int = 0
102
+
103
+ def __post_init__(self):
104
+ # Calculate statistics
105
+ self.total_tool_calls = sum(len(step.tool_calls) for step in self.steps)
106
+ self.successful_tool_calls = sum(
107
+ len([r for r in step.tool_results if r.is_success]) for step in self.steps
108
+ )
109
+ self.failed_tool_calls = sum(
110
+ len([r for r in step.tool_results if r.is_error]) for step in self.steps
111
+ )
112
+
113
+ @property
114
+ def final_response(self) -> str:
115
+ """Get the final text response from the conversation."""
116
+ if self.steps:
117
+ return self.steps[-1].text
118
+ return ""
119
+
120
+ @property
121
+ def has_errors(self) -> bool:
122
+ """Check if any steps have errors."""
123
+ return any(step.has_errors for step in self.steps)
124
+
125
+ @property
126
+ def success_rate(self) -> float:
127
+ """Calculate tool call success rate."""
128
+ if self.total_tool_calls == 0:
129
+ return 1.0
130
+ return self.successful_tool_calls / self.total_tool_calls
@@ -0,0 +1,15 @@
1
+ """Multi-server orchestration tools."""
2
+
3
+ from .config_loader import ConfigLoader
4
+ from .server_orchestrator import ServerOrchestrator, get_orchestrator
5
+ from .server_registry import ServerRegistry, get_registry
6
+ from .server_selector import ServerSelector
7
+
8
+ __all__ = [
9
+ "ServerOrchestrator",
10
+ "get_orchestrator",
11
+ "ServerRegistry",
12
+ "get_registry",
13
+ "ConfigLoader",
14
+ "ServerSelector",
15
+ ]