Skip to content

Commit 1437bc5

Browse files
author
Zvi Fried
committed
fixes
1 parent 42bc79d commit 1437bc5

24 files changed

+1070
-725
lines changed

src/mcp_as_a_judge/coding_task_manager.py

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import json
99
import time
1010

11+
from pydantic import ValidationError
12+
1113
from mcp_as_a_judge.db.conversation_history_service import ConversationHistoryService
1214
from mcp_as_a_judge.logging_config import get_logger
1315
from mcp_as_a_judge.models.task_metadata import TaskMetadata, TaskSize, TaskState
@@ -140,14 +142,18 @@ async def load_task_metadata_from_history(
140142
session_id=task_id
141143
)
142144

143-
# Look for the most recent task metadata record
145+
# Look for the most recent task metadata record from any source
146+
# (not just set_coding_task, since other tools also save task metadata)
144147
for record in reversed(conversation_history):
145-
if record.source == "set_coding_task" and "task_metadata" in record.output:
146-
# Parse the task metadata from the record
148+
try:
149+
# Parse the record output to look for task metadata
147150
output_data = json.loads(record.output)
148151
if "current_task_metadata" in output_data:
149152
metadata_dict = output_data["current_task_metadata"]
150153
return TaskMetadata.model_validate(metadata_dict)
154+
except (json.JSONDecodeError, ValidationError):
155+
# Skip records that can't be parsed or validated
156+
continue
151157

152158
return None
153159

@@ -177,14 +183,18 @@ async def save_task_metadata_to_history(
177183
session_id=task_metadata.task_id,
178184
tool_name="set_coding_task",
179185
tool_input=user_request,
180-
tool_output=json.dumps({
181-
"action": action,
182-
"current_task_metadata": task_metadata.model_dump(mode='json'),
183-
"timestamp": int(time.time()),
184-
}),
186+
tool_output=json.dumps(
187+
{
188+
"action": action,
189+
"current_task_metadata": task_metadata.model_dump(mode="json"),
190+
"timestamp": int(time.time()),
191+
}
192+
),
185193
)
186194

187-
logger.info(f"💾 Saved task metadata to conversation history: {task_metadata.task_id}")
195+
logger.info(
196+
f"💾 Saved task metadata to conversation history: {task_metadata.task_id}"
197+
)
188198

189199
except Exception as e:
190200
logger.error(f"❌ Failed to save task metadata to history: {e}")
@@ -205,12 +215,42 @@ def validate_state_transition(current_state: TaskState, new_state: TaskState) ->
205215
# Define valid state transitions
206216
valid_transitions = {
207217
TaskState.CREATED: [TaskState.PLANNING, TaskState.BLOCKED, TaskState.CANCELLED],
208-
TaskState.PLANNING: [TaskState.PLAN_APPROVED, TaskState.CREATED, TaskState.BLOCKED, TaskState.CANCELLED],
209-
TaskState.PLAN_APPROVED: [TaskState.IMPLEMENTING, TaskState.PLANNING, TaskState.BLOCKED, TaskState.CANCELLED],
210-
TaskState.IMPLEMENTING: [TaskState.IMPLEMENTING, TaskState.REVIEW_READY, TaskState.PLAN_APPROVED, TaskState.BLOCKED, TaskState.CANCELLED],
211-
TaskState.REVIEW_READY: [TaskState.COMPLETED, TaskState.IMPLEMENTING, TaskState.BLOCKED, TaskState.CANCELLED],
212-
TaskState.COMPLETED: [TaskState.CANCELLED], # Only allow cancellation of completed tasks
213-
TaskState.BLOCKED: [TaskState.CREATED, TaskState.PLANNING, TaskState.PLAN_APPROVED, TaskState.IMPLEMENTING, TaskState.REVIEW_READY, TaskState.CANCELLED],
218+
TaskState.PLANNING: [
219+
TaskState.PLAN_APPROVED,
220+
TaskState.CREATED,
221+
TaskState.BLOCKED,
222+
TaskState.CANCELLED,
223+
],
224+
TaskState.PLAN_APPROVED: [
225+
TaskState.IMPLEMENTING,
226+
TaskState.PLANNING,
227+
TaskState.BLOCKED,
228+
TaskState.CANCELLED,
229+
],
230+
TaskState.IMPLEMENTING: [
231+
TaskState.IMPLEMENTING,
232+
TaskState.REVIEW_READY,
233+
TaskState.PLAN_APPROVED,
234+
TaskState.BLOCKED,
235+
TaskState.CANCELLED,
236+
],
237+
TaskState.REVIEW_READY: [
238+
TaskState.COMPLETED,
239+
TaskState.IMPLEMENTING,
240+
TaskState.BLOCKED,
241+
TaskState.CANCELLED,
242+
],
243+
TaskState.COMPLETED: [
244+
TaskState.CANCELLED
245+
], # Only allow cancellation of completed tasks
246+
TaskState.BLOCKED: [
247+
TaskState.CREATED,
248+
TaskState.PLANNING,
249+
TaskState.PLAN_APPROVED,
250+
TaskState.IMPLEMENTING,
251+
TaskState.REVIEW_READY,
252+
TaskState.CANCELLED,
253+
],
214254
TaskState.CANCELLED: [], # No transitions from cancelled state
215255
}
216256

src/mcp_as_a_judge/constants.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@
55
"""
66

77
# LLM Configuration
8-
MAX_TOKENS = 25000 # Maximum tokens for all LLM requests - increased for comprehensive responses
8+
MAX_TOKENS = (
9+
25000 # Maximum tokens for all LLM requests - increased for comprehensive responses
10+
)
911
DEFAULT_TEMPERATURE = 0.1 # Default temperature for LLM requests
10-
DEFAULT_REASONING_EFFORT = "low" # Default reasoning effort level - lowest for speed and efficiency
12+
DEFAULT_REASONING_EFFORT = (
13+
"low" # Default reasoning effort level - lowest for speed and efficiency
14+
)
1115

1216
# Timeout Configuration
1317
DEFAULT_TIMEOUT = 30 # Default timeout in seconds for operations

src/mcp_as_a_judge/db/interface.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
must implement for storing and retrieving conversation history.
66
"""
77

8-
from abc import ABC, abstractmethod
98
import time
9+
from abc import ABC, abstractmethod
1010

1111
from sqlmodel import Field, SQLModel
1212

src/mcp_as_a_judge/db/providers/sqlite_provider.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
It supports both in-memory (:memory:) and file-based SQLite storage.
66
"""
77

8-
import uuid
98
import time
9+
import uuid
1010

1111
from sqlalchemy import create_engine
1212
from sqlmodel import Session, SQLModel, asc, desc, select

src/mcp_as_a_judge/elicitation_provider.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ async def _elicit(
7373
if elicit_result.action == "accept" and elicit_result.data:
7474
# Convert Pydantic model to dictionary
7575
if hasattr(elicit_result.data, "model_dump"):
76-
data = elicit_result.data.model_dump()
76+
data = elicit_result.data.model_dump(exclude_none=True)
7777
elif isinstance(elicit_result.data, dict): # type: ignore[unreachable]
7878
data = elicit_result.data # type: ignore[unreachable]
7979
else:
@@ -122,7 +122,8 @@ async def _elicit(
122122

123123
# Generate fallback message using prompt template
124124
fallback_message = prompt_loader.render_prompt(
125-
"user/elicitation_fallback.md", **template_vars.model_dump()
125+
"user/elicitation_fallback.md",
126+
**template_vars.model_dump(exclude_none=True),
126127
)
127128

128129
return ElicitationResult(success=False, message=fallback_message)

src/mcp_as_a_judge/logging_config.py

Lines changed: 132 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,55 @@
11
"""
22
Central logging configuration for MCP as a Judge.
33
4-
This module provides centralized logging setup with colored output and
5-
consistent formatting across the entire application.
4+
This module provides centralized logging setup with MCP Context integration
5+
when available, falling back to standard logging otherwise.
66
"""
77

88
import logging
99
import sys
1010
from datetime import datetime
11-
from typing import Any, ClassVar
11+
from typing import Any
1212

13+
# Import MCP SDK logging utilities for proper color support
14+
try:
15+
from mcp.server.fastmcp.utilities.logging import (
16+
configure_logging,
17+
)
18+
from mcp.server.fastmcp.utilities.logging import (
19+
get_logger as mcp_get_logger,
20+
)
1321

14-
class ColoredFormatter(logging.Formatter):
15-
"""Custom formatter that adds colors to log levels and formats messages."""
22+
MCP_SDK_AVAILABLE = True
23+
except ImportError:
24+
configure_logging = None # type: ignore[assignment]
25+
mcp_get_logger = None # type: ignore[assignment]
26+
MCP_SDK_AVAILABLE = False
1627

17-
# ANSI color codes
18-
COLORS: ClassVar[dict[str, str]] = {
19-
"DEBUG": "\033[36m", # Cyan
20-
"INFO": "\033[32m", # Green
21-
"WARNING": "\033[33m", # Yellow
22-
"ERROR": "\033[31m", # Red
23-
"CRITICAL": "\033[35m", # Magenta
24-
}
25-
RESET = "\033[0m" # Reset color
28+
# Global context reference for MCP integration
29+
_global_context_ref: Any | None = None
2630

27-
def format(self, record: logging.LogRecord) -> str:
28-
"""Format log record with colors and custom format."""
29-
# Get color for log level
30-
level_color = self.COLORS.get(record.levelname, "")
3131

32-
# Format: [level_with_color] [module:linenumber] [iso-date] [message]
33-
colored_level = f"{level_color}{record.levelname}{self.RESET}"
32+
def set_context_reference(ctx: Any) -> None:
33+
"""Set global context reference for MCP integration."""
34+
global _global_context_ref
35+
_global_context_ref = ctx
36+
3437

35-
# Get module name (remove package prefix for cleaner output)
38+
class CleanFormatter(logging.Formatter):
39+
"""Clean formatter without ANSI colors - uses MCP SDK logging for proper color support."""
40+
41+
def format(self, record: logging.LogRecord) -> str:
42+
"""Format log record with clean output."""
43+
# Get module name from the actual caller (remove package prefix for cleaner output)
3644
module_name = record.name
3745
if module_name.startswith("mcp_as_a_judge."):
3846
module_name = module_name[len("mcp_as_a_judge.") :]
3947

4048
# Format timestamp as ISO date
4149
timestamp = datetime.fromtimestamp(record.created).isoformat()
4250

43-
# Build the formatted message
44-
formatted_message = f"[{colored_level}] [{module_name}:{record.lineno}] [{timestamp}] {record.getMessage()}"
51+
# Clean format without ANSI codes - let MCP SDK handle colors
52+
formatted_message = f"[{record.levelname}] [{module_name}:{record.lineno}] [{timestamp}] {record.getMessage()}"
4553

4654
# Handle exceptions
4755
if record.exc_info:
@@ -50,32 +58,34 @@ def format(self, record: logging.LogRecord) -> str:
5058
return formatted_message
5159

5260

53-
def setup_logging(level: int = logging.INFO) -> None:
61+
def setup_logging(level: str = "INFO") -> None:
5462
"""
55-
Set up centralized logging configuration for the entire application.
63+
Set up centralized logging configuration using MCP SDK.
5664
5765
Args:
58-
level: Logging level (default: INFO)
66+
level: Logging level (default: "INFO")
5967
"""
60-
# Create custom formatter
61-
formatter = ColoredFormatter()
62-
63-
# Create handler for stderr (so it's visible in development tools like Cursor)
64-
handler = logging.StreamHandler(sys.stderr)
65-
handler.setFormatter(formatter)
68+
if MCP_SDK_AVAILABLE and configure_logging is not None:
69+
# Use MCP SDK configure_logging for proper color support
70+
configure_logging(level) # type: ignore[misc]
71+
else:
72+
# Fallback to standard logging setup
73+
# Create custom formatter
74+
formatter = CleanFormatter()
6675

67-
# Configure root logger
68-
root_logger = logging.getLogger()
69-
root_logger.setLevel(level)
76+
# Create handler for stderr (so it's visible in development tools like Cursor)
77+
handler = logging.StreamHandler(sys.stderr)
78+
handler.setFormatter(formatter)
7079

71-
# Clear any existing handlers to avoid duplicates
72-
root_logger.handlers.clear()
80+
# Configure root logger
81+
root_logger = logging.getLogger()
82+
root_logger.setLevel(getattr(logging, level.upper(), logging.INFO))
7383

74-
# Add our custom handler
75-
root_logger.addHandler(handler)
84+
# Clear any existing handlers to avoid duplicates
85+
root_logger.handlers.clear()
7686

77-
# Configure specific loggers for our application
78-
configure_application_loggers(level)
87+
# Add our custom handler
88+
root_logger.addHandler(handler)
7989

8090

8191
def configure_application_loggers(level: int = logging.INFO) -> None:
@@ -105,6 +115,87 @@ def configure_application_loggers(level: int = logging.INFO) -> None:
105115
logger.setLevel(level)
106116

107117

118+
class ContextAwareLogger:
119+
"""Logger that automatically uses MCP Context when available."""
120+
121+
def __init__(self, name: str):
122+
"""Initialize logger with a name."""
123+
self.name = name
124+
# Use MCP SDK logger for fallback (proper color support)
125+
if MCP_SDK_AVAILABLE:
126+
# Clean name for MCP SDK
127+
clean_name = name
128+
if name.startswith("mcp_as_a_judge."):
129+
clean_name = name[len("mcp_as_a_judge.") :]
130+
elif name == "__main__":
131+
clean_name = "server"
132+
self._fallback_logger = mcp_get_logger(clean_name)
133+
else:
134+
self._fallback_logger = logging.getLogger(name)
135+
136+
async def info(self, message: str) -> None:
137+
"""Log info message using Context if available, MCP SDK logging otherwise."""
138+
global _global_context_ref
139+
if _global_context_ref is not None:
140+
await _global_context_ref.info(message)
141+
else:
142+
self._fallback_logger.info(message)
143+
144+
async def debug(self, message: str) -> None:
145+
"""Log debug message using Context if available, MCP SDK logging otherwise."""
146+
global _global_context_ref
147+
if _global_context_ref is not None:
148+
await _global_context_ref.debug(message)
149+
else:
150+
self._fallback_logger.debug(message)
151+
152+
async def warning(self, message: str) -> None:
153+
"""Log warning message using Context if available, MCP SDK logging otherwise."""
154+
global _global_context_ref
155+
if _global_context_ref is not None:
156+
await _global_context_ref.warning(message)
157+
else:
158+
self._fallback_logger.warning(message)
159+
160+
async def error(self, message: str) -> None:
161+
"""Log error message using Context if available, MCP SDK logging otherwise."""
162+
global _global_context_ref
163+
if _global_context_ref is not None:
164+
await _global_context_ref.error(message)
165+
else:
166+
self._fallback_logger.error(message)
167+
168+
# Synchronous methods for backward compatibility
169+
def info_sync(self, message: str) -> None:
170+
"""Synchronous info logging using MCP SDK."""
171+
self._fallback_logger.info(message)
172+
173+
def debug_sync(self, message: str) -> None:
174+
"""Synchronous debug logging using MCP SDK."""
175+
self._fallback_logger.debug(message)
176+
177+
def warning_sync(self, message: str) -> None:
178+
"""Synchronous warning logging using MCP SDK."""
179+
self._fallback_logger.warning(message)
180+
181+
def error_sync(self, message: str) -> None:
182+
"""Synchronous error logging using MCP SDK."""
183+
self._fallback_logger.error(message)
184+
185+
186+
def get_context_aware_logger(name: str) -> ContextAwareLogger:
187+
"""
188+
Get a context-aware logger that automatically uses MCP Context when available.
189+
190+
Args:
191+
name: Logger name (typically __name__ from the calling module)
192+
193+
Returns:
194+
ContextAwareLogger instance
195+
"""
196+
return ContextAwareLogger(name)
197+
198+
108199
def get_logger(name: str) -> logging.Logger:
109200
"""
110201
Get a logger instance with the given name.

0 commit comments

Comments
 (0)