Skip to content

Commit 09ce62a

Browse files
authored
Merge pull request #29 from redis/fix/fix-mcp-logging
Stop logging to sdtio with mcp server in stdio mode
2 parents c98ebe1 + 38f836e commit 09ce62a

File tree

6 files changed

+86
-43
lines changed

6 files changed

+86
-43
lines changed

agent_memory_server/cli.py

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1-
#!/usr/bin/env python
21
"""
32
Command-line interface for agent-memory-server.
43
"""
54

65
import datetime
76
import importlib
8-
import logging
97
import sys
108

119
import click
1210
import uvicorn
1311

1412
from agent_memory_server.config import settings
15-
from agent_memory_server.logging import configure_logging, get_logger
13+
from agent_memory_server.logging import (
14+
configure_logging,
15+
configure_mcp_logging,
16+
get_logger,
17+
)
1618
from agent_memory_server.migrations import (
1719
migrate_add_discrete_memory_extracted_2,
1820
migrate_add_memory_hashes_1,
@@ -21,12 +23,7 @@
2123
from agent_memory_server.utils.redis import ensure_search_index_exists, get_redis_conn
2224

2325

24-
# Don't configure logging at module level - let each command handle it
25-
def _get_logger():
26-
"""Get logger instance after ensuring logging is configured."""
27-
configure_logging()
28-
return get_logger(__name__)
29-
26+
logger = get_logger(__name__)
3027

3128
VERSION = "0.2.0"
3229

@@ -48,6 +45,8 @@ def rebuild_index():
4845
"""Rebuild the search index."""
4946
import asyncio
5047

48+
configure_logging()
49+
5150
async def setup_and_run():
5251
redis = await get_redis_conn()
5352
await ensure_search_index_exists(redis, overwrite=True)
@@ -60,6 +59,7 @@ def migrate_memories():
6059
"""Migrate memories from the old format to the new format."""
6160
import asyncio
6261

62+
configure_logging()
6363
click.echo("Starting memory migrations...")
6464

6565
async def run_migrations():
@@ -85,6 +85,7 @@ def api(port: int, host: str, reload: bool):
8585
"""Run the REST API server."""
8686
from agent_memory_server.main import on_start_logger
8787

88+
configure_logging()
8889
on_start_logger(port)
8990
uvicorn.run(
9091
"agent_memory_server.main:app",
@@ -106,6 +107,12 @@ def mcp(port: int, mode: str):
106107
"""Run the MCP server."""
107108
import asyncio
108109

110+
# Configure MCP-specific logging BEFORE any imports to avoid stdout contamination
111+
if mode == "stdio":
112+
configure_mcp_logging()
113+
else:
114+
configure_logging()
115+
109116
# Update the port in settings FIRST
110117
settings.mcp_port = port
111118

@@ -117,17 +124,10 @@ async def setup_and_run():
117124

118125
# Run the MCP server
119126
if mode == "sse":
120-
_get_logger().info(f"Starting MCP server on port {port}\n")
127+
logger.info(f"Starting MCP server on port {port}\n")
121128
await mcp_app.run_sse_async()
122129
elif mode == "stdio":
123-
# Try to force all logging to stderr because stdio-mode MCP servers
124-
# use standard output for the protocol.
125-
logging.basicConfig(
126-
level=settings.log_level,
127-
stream=sys.stderr,
128-
force=True, # remove any existing handlers
129-
format="%(asctime)s %(name)s %(levelname)s %(message)s",
130-
)
130+
# Logging already configured above
131131
await mcp_app.run_stdio_async()
132132
else:
133133
raise ValueError(f"Invalid mode: {mode}")
@@ -157,6 +157,8 @@ def schedule_task(task_path: str, args: list[str]):
157157

158158
from docket import Docket
159159

160+
configure_logging()
161+
160162
# Parse the arguments
161163
task_args = {}
162164
for arg in args:
@@ -222,6 +224,8 @@ def task_worker(concurrency: int, redelivery_timeout: int):
222224

223225
from docket import Worker
224226

227+
configure_logging()
228+
225229
if not settings.use_docket:
226230
click.echo("Docket is disabled in settings. Cannot run worker.")
227231
sys.exit(1)

agent_memory_server/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ class Settings(BaseSettings):
115115

116116
# Other Application settings
117117
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
118+
default_mcp_user_id: str | None = None
119+
default_mcp_namespace: str | None = None
118120

119121
class Config:
120122
env_file = ".env"

agent_memory_server/logging.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def configure_logging():
1717

1818
# Configure standard library logging based on settings.log_level
1919
level = getattr(logging, settings.log_level.upper(), logging.INFO)
20-
handler = logging.StreamHandler(sys.stdout)
20+
handler = logging.StreamHandler(sys.stderr) # Use stderr instead of stdout
2121
handler.setLevel(level)
2222
logging.basicConfig(level=level, handlers=[handler], format="%(message)s")
2323

@@ -38,6 +38,41 @@ def configure_logging():
3838
_configured = True
3939

4040

41+
def configure_mcp_logging():
42+
"""Configure logging specifically for MCP server in stdio mode"""
43+
global _configured
44+
45+
# Clear any existing handlers and configuration
46+
root_logger = logging.getLogger()
47+
root_logger.handlers.clear()
48+
49+
# Configure stderr-only logging for MCP stdio mode
50+
level = getattr(logging, settings.log_level.upper(), logging.INFO)
51+
stderr_handler = logging.StreamHandler(sys.stderr)
52+
stderr_handler.setLevel(level)
53+
stderr_handler.setFormatter(
54+
logging.Formatter("%(asctime)s %(name)s %(levelname)s %(message)s")
55+
)
56+
root_logger.addHandler(stderr_handler)
57+
root_logger.setLevel(level)
58+
59+
# Configure structlog to also use stderr
60+
structlog.configure(
61+
processors=[
62+
structlog.stdlib.filter_by_level,
63+
structlog.stdlib.add_logger_name,
64+
structlog.stdlib.add_log_level,
65+
structlog.processors.TimeStamper(fmt="iso"),
66+
structlog.processors.format_exc_info,
67+
structlog.processors.JSONRenderer(),
68+
],
69+
wrapper_class=structlog.stdlib.BoundLogger,
70+
logger_factory=structlog.stdlib.LoggerFactory(),
71+
cache_logger_on_first_use=True,
72+
)
73+
_configured = True
74+
75+
4176
def get_logger(name: str | None = None) -> structlog.stdlib.BoundLogger:
4277
"""
4378
Get a configured logger instance.

agent_memory_server/mcp.py

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import logging
2-
import os
32
from typing import Any
43

54
import ulid
@@ -43,9 +42,6 @@
4342

4443
logger = logging.getLogger(__name__)
4544

46-
# Default namespace for STDIO mode
47-
DEFAULT_NAMESPACE = os.getenv("MCP_NAMESPACE")
48-
4945

5046
class FastMCP(_FastMCPBase):
5147
"""Extend FastMCP to support optional URL namespace and default STDIO namespace."""
@@ -165,7 +161,7 @@ async def run_stdio_async(self):
165161
"Redis Agent Memory Server",
166162
port=settings.mcp_port,
167163
instructions=INSTRUCTIONS,
168-
default_namespace=DEFAULT_NAMESPACE,
164+
default_namespace=settings.default_mcp_namespace,
169165
)
170166

171167

@@ -301,10 +297,11 @@ async def create_long_term_memories(
301297
An acknowledgement response indicating success
302298
"""
303299
# Apply default namespace for STDIO if not provided in memory entries
304-
if DEFAULT_NAMESPACE:
305-
for mem in memories:
306-
if mem.namespace is None:
307-
mem.namespace = DEFAULT_NAMESPACE
300+
for mem in memories:
301+
if mem.namespace is None and settings.default_mcp_namespace:
302+
mem.namespace = settings.default_mcp_namespace
303+
if mem.user_id is None and settings.default_mcp_user_id:
304+
mem.user_id = settings.default_mcp_user_id
308305

309306
payload = CreateMemoryRecordRequest(
310307
memories=[MemoryRecord(**mem.model_dump()) for mem in memories]
@@ -418,6 +415,11 @@ async def search_long_term_memory(
418415
Returns:
419416
MemoryRecordResults containing matched memories sorted by relevance
420417
"""
418+
if user_id is None and settings.default_mcp_user_id:
419+
user_id = UserId(eq=settings.default_mcp_user_id)
420+
if namespace is None and settings.default_mcp_namespace:
421+
namespace = Namespace(eq=settings.default_mcp_namespace)
422+
421423
try:
422424
payload = SearchRequest(
423425
text=text,
@@ -545,7 +547,7 @@ async def memory_prompt(
545547
546548
Args:
547549
- text: The user's query
548-
- session_id: Add conversation history from a session
550+
- session_id: Add conversation history from a working memory session
549551
- namespace: Filter session and long-term memory namespace
550552
- topics: Search for long-term memories matching topics
551553
- entities: Search for long-term memories matching entities
@@ -562,6 +564,9 @@ async def memory_prompt(
562564
_session_id = session_id.eq if session_id and session_id.eq else None
563565
session = None
564566

567+
if user_id is None and settings.default_mcp_user_id:
568+
user_id = UserId(eq=settings.default_mcp_user_id)
569+
565570
if _session_id is not None:
566571
session = WorkingMemoryRequest(
567572
session_id=_session_id,
@@ -602,8 +607,8 @@ async def set_working_memory(
602607
messages: list[MemoryMessage] | None = None,
603608
context: str | None = None,
604609
data: dict[str, Any] | None = None,
605-
namespace: str | None = None,
606-
user_id: str | None = None,
610+
namespace: str | None = settings.default_mcp_namespace,
611+
user_id: str | None = settings.default_mcp_user_id,
607612
ttl_seconds: int = 3600,
608613
) -> WorkingMemoryResponse:
609614
"""
@@ -683,11 +688,6 @@ async def set_working_memory(
683688
Returns:
684689
Updated working memory response (may include summarization if window exceeded)
685690
"""
686-
# Apply default namespace if configured
687-
memory_namespace = namespace
688-
if not memory_namespace and DEFAULT_NAMESPACE:
689-
memory_namespace = DEFAULT_NAMESPACE
690-
691691
# Auto-generate IDs for memories that don't have them
692692
processed_memories = []
693693
if memories:
@@ -699,6 +699,7 @@ async def set_working_memory(
699699
processed_memory = memory.model_copy(
700700
update={
701701
"id": memory_id,
702+
"user_id": user_id,
702703
"persisted_at": None, # Mark as pending promotion
703704
}
704705
)
@@ -715,7 +716,7 @@ async def set_working_memory(
715716
# Create the working memory object
716717
working_memory_obj = WorkingMemory(
717718
session_id=session_id,
718-
namespace=memory_namespace,
719+
namespace=namespace,
719720
memories=processed_memories,
720721
messages=messages or [],
721722
context=context,

agent_memory_server/vectorstore_factory.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,10 @@ def patched_create_ulid() -> str:
3838

3939
# Replace the broken function with our working one
4040
redisvl.utils.utils.create_ulid = patched_create_ulid
41-
logging.info("Successfully patched RedisVL ULID function")
42-
except Exception as e:
43-
logging.warning(f"Could not patch RedisVL ULID function: {e}")
41+
# Note: Successfully patched RedisVL ULID function
42+
except Exception:
43+
# Note: Could not patch RedisVL ULID function
44+
pass
4445

4546
from agent_memory_server.config import settings
4647
from agent_memory_server.vectorstore_adapter import (

tests/test_cli.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,11 +157,11 @@ def test_mcp_command_sse_mode(self, mock_mcp_app, mock_settings):
157157
assert result.exit_code == 0
158158
mock_mcp_app.run_sse_async.assert_called_once()
159159

160-
@patch("agent_memory_server.cli.logging.basicConfig")
160+
@patch("agent_memory_server.cli.configure_mcp_logging")
161161
@patch("agent_memory_server.cli.settings")
162162
@patch("agent_memory_server.mcp.mcp_app")
163163
def test_mcp_command_stdio_logging_config(
164-
self, mock_mcp_app, mock_settings, mock_basic_config
164+
self, mock_mcp_app, mock_settings, mock_configure_mcp_logging
165165
):
166166
"""Test that stdio mode configures logging to stderr."""
167167
mock_settings.mcp_port = 3001
@@ -174,7 +174,7 @@ def test_mcp_command_stdio_logging_config(
174174

175175
assert result.exit_code == 0
176176
mock_mcp_app.run_stdio_async.assert_called_once()
177-
mock_basic_config.assert_called_once()
177+
mock_configure_mcp_logging.assert_called_once()
178178

179179

180180
class TestScheduleTask:

0 commit comments

Comments
 (0)