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.
- common/__init__.py +21 -0
- common/types.py +182 -0
- lightfast_mcp/__init__.py +50 -0
- lightfast_mcp/core/__init__.py +14 -0
- lightfast_mcp/core/base_server.py +205 -0
- lightfast_mcp/exceptions.py +55 -0
- lightfast_mcp/servers/__init__.py +1 -0
- lightfast_mcp/servers/blender/__init__.py +5 -0
- lightfast_mcp/servers/blender/server.py +358 -0
- lightfast_mcp/servers/blender_mcp_server.py +82 -0
- lightfast_mcp/servers/mock/__init__.py +5 -0
- lightfast_mcp/servers/mock/server.py +101 -0
- lightfast_mcp/servers/mock/tools.py +161 -0
- lightfast_mcp/servers/mock_server.py +78 -0
- lightfast_mcp/utils/__init__.py +1 -0
- lightfast_mcp/utils/logging_utils.py +69 -0
- mseep_lightfast_mcp-0.0.1.dist-info/METADATA +36 -0
- mseep_lightfast_mcp-0.0.1.dist-info/RECORD +43 -0
- mseep_lightfast_mcp-0.0.1.dist-info/WHEEL +5 -0
- mseep_lightfast_mcp-0.0.1.dist-info/entry_points.txt +7 -0
- mseep_lightfast_mcp-0.0.1.dist-info/licenses/LICENSE +21 -0
- mseep_lightfast_mcp-0.0.1.dist-info/top_level.txt +3 -0
- tools/__init__.py +46 -0
- tools/ai/__init__.py +8 -0
- tools/ai/conversation_cli.py +345 -0
- tools/ai/conversation_client.py +399 -0
- tools/ai/conversation_session.py +342 -0
- tools/ai/providers/__init__.py +11 -0
- tools/ai/providers/base_provider.py +64 -0
- tools/ai/providers/claude_provider.py +200 -0
- tools/ai/providers/openai_provider.py +204 -0
- tools/ai/tool_executor.py +257 -0
- tools/common/__init__.py +99 -0
- tools/common/async_utils.py +419 -0
- tools/common/errors.py +222 -0
- tools/common/logging.py +252 -0
- tools/common/types.py +130 -0
- tools/orchestration/__init__.py +15 -0
- tools/orchestration/cli.py +320 -0
- tools/orchestration/config_loader.py +348 -0
- tools/orchestration/server_orchestrator.py +466 -0
- tools/orchestration/server_registry.py +187 -0
- tools/orchestration/server_selector.py +242 -0
tools/common/logging.py
ADDED
|
@@ -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
|
+
]
|