Skip to content

Commit 23e0073

Browse files
committed
feat: improve mcp log clarity
Signed-off-by: phernandez <paul@basicmachines.co>
1 parent b1e8b9e commit 23e0073

7 files changed

Lines changed: 126 additions & 30 deletions

File tree

src/basic_memory/mcp/project_context.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,22 @@ def _canonicalize_project_name(
8585
return project_name
8686

8787

88+
def _configured_project_name(
89+
project_name: Optional[str],
90+
config: BasicMemoryConfig,
91+
) -> Optional[str]:
92+
"""Return the configured project name when the identifier matches a known project."""
93+
if project_name is None:
94+
return None
95+
96+
requested_permalink = generate_permalink(project_name)
97+
for configured_name in config.projects:
98+
if generate_permalink(configured_name) == requested_permalink:
99+
return configured_name
100+
101+
return None
102+
103+
88104
async def resolve_project_parameter(
89105
project: Optional[str] = None,
90106
allow_discovery: bool = False,
@@ -363,7 +379,8 @@ async def resolve_project_and_path(
363379
Tuple of (active_project, normalized_path, is_memory_url)
364380
"""
365381
is_memory_url = identifier.strip().startswith("memory://")
366-
include_project = ConfigManager().config.permalinks_include_project if is_memory_url else None
382+
config = ConfigManager().config
383+
include_project = config.permalinks_include_project if is_memory_url else None
367384
with telemetry.scope(
368385
"routing.resolve_memory_url",
369386
is_memory_url=is_memory_url,
@@ -376,12 +393,20 @@ async def resolve_project_and_path(
376393

377394
normalized_path = normalize_project_reference(memory_url_path(identifier))
378395
project_prefix, remainder = _split_project_prefix(normalized_path)
379-
include_project = ConfigManager().config.permalinks_include_project
396+
include_project = config.permalinks_include_project
397+
configured_prefix = _configured_project_name(project_prefix, config)
380398

381399
# Trigger: memory URL begins with a potential project segment
382400
# Why: allow project-scoped memory URLs without requiring a separate project parameter
383401
# Outcome: attempt to resolve the prefix as a project and route to it
384-
if project_prefix:
402+
#
403+
# Trigger: the active project is already fixed and the first path segment is not
404+
# a configured project name
405+
# Why: in memory URLs like memory://notes/topic, "notes" is a directory, not a
406+
# project. Resolving it through /v2/projects/resolve creates noisy false-negative
407+
# logs before the tool recovers with the active project anyway.
408+
# Outcome: skip project resolution and keep the path within the active project.
409+
if project_prefix and (configured_prefix is not None or project is None):
385410
try:
386411
from basic_memory.mcp.tools.utils import call_post
387412

src/basic_memory/mcp/tools/build_context.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -223,15 +223,17 @@ async def build_context(
223223
tool_name="build_context",
224224
):
225225
logger.info(
226-
"Building context",
227-
url=str(url),
228-
project=active_project.name,
229-
depth=depth,
230-
timeframe=timeframe,
226+
f"MCP tool call tool=build_context project={active_project.name} "
227+
f"url={url} depth={depth} timeframe={timeframe} output_format={output_format}"
231228
)
232229

233230
# Resolve memory:// identifier with project-prefix awareness
234-
_, resolved_path, _ = await resolve_project_and_path(client, url, project, context)
231+
_, resolved_path, _ = await resolve_project_and_path(
232+
client,
233+
url,
234+
active_project.name,
235+
context,
236+
)
235237

236238
# Import here to avoid circular import
237239
from basic_memory.mcp.clients import MemoryClient
@@ -247,6 +249,14 @@ async def build_context(
247249
max_related=max_related,
248250
)
249251

252+
logger.info(
253+
f"MCP tool response: tool=build_context project={active_project.name} "
254+
f"uri={graph.metadata.uri or resolved_path} "
255+
f"primary_count={graph.metadata.primary_count or 0} "
256+
f"related_count={graph.metadata.related_count or 0} "
257+
f"output_format={output_format}"
258+
)
259+
250260
if output_format == "text":
251261
return _format_context_markdown(graph, active_project.name)
252262

src/basic_memory/mcp/tools/edit_note.py

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -290,11 +290,8 @@ async def edit_note(
290290
tool_name="edit_note",
291291
):
292292
logger.info(
293-
"MCP tool call",
294-
tool="edit_note",
295-
project=active_project.name,
296-
identifier=identifier,
297-
operation=operation,
293+
f"MCP tool call tool=edit_note project={active_project.name} "
294+
f"identifier={identifier} operation={operation} output_format={output_format}"
298295
)
299296

300297
# Validate operation
@@ -474,14 +471,11 @@ async def edit_note(
474471
summary.append(f"- Unresolved: {unresolved}")
475472

476473
logger.info(
477-
"MCP tool response",
478-
tool="edit_note",
479-
operation=operation,
480-
project=active_project.name,
481-
permalink=result.permalink,
482-
observations_count=len(result.observations),
483-
relations_count=len(result.relations),
484-
file_created=file_created,
474+
f"MCP tool response: tool=edit_note project={active_project.name} "
475+
f"operation={operation} permalink={result.permalink} "
476+
f"observations_count={len(result.observations)} "
477+
f"relations_count={len(result.relations)} "
478+
f"file_created={str(file_created).lower()}"
485479
)
486480

487481
if output_format == "json":

src/basic_memory/mcp/tools/move_note.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -477,8 +477,11 @@ async def move_note(
477477
}
478478
return f"# Move Failed - Invalid Parameters\n\n{error_msg}"
479479
async with get_project_client(project, workspace, context) as (client, active_project):
480-
logger.debug(
481-
f"Moving {'directory' if is_directory else 'note'}: {identifier} to {destination_path} in project: {active_project.name}"
480+
destination_target = destination_folder or destination_path
481+
logger.info(
482+
f"MCP tool call tool=move_note project={active_project.name} "
483+
f"identifier={identifier} destination={destination_target} "
484+
f"is_directory={str(is_directory).lower()}"
482485
)
483486

484487
# Validate destination path to prevent path traversal attacks
@@ -834,10 +837,8 @@ async def _ensure_resolved_entity_id() -> str:
834837

835838
# Log the operation
836839
logger.info(
837-
"Move note completed",
838-
identifier=identifier,
839-
destination_path=destination_path,
840-
project=active_project.name,
840+
f"MCP tool response: tool=move_note project={active_project.name} "
841+
f"source={identifier} destination={result.file_path} permalink={result.permalink}"
841842
)
842843

843844
return "\n".join(result_lines)

src/basic_memory/mcp/tools/read_content.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ async def read_content(
216216
if detected:
217217
project = detected
218218

219-
logger.info("Reading file", path=path, project=project)
219+
logger.info(f"MCP tool call tool=read_content project={project} path={path}")
220220

221221
async with get_project_client(project, workspace, context) as (client, active_project):
222222
# Resolve path with project-prefix awareness for memory:// URLs
@@ -260,6 +260,10 @@ async def read_content(
260260
# Handle text or json
261261
if content_type.startswith("text/") or content_type == "application/json":
262262
logger.debug("Processing text resource")
263+
logger.info(
264+
f"MCP tool response: tool=read_content project={active_project.name} "
265+
f"path={url} type=text content_type={content_type}"
266+
)
263267
return {
264268
"type": "text",
265269
"text": response.text,
@@ -272,6 +276,10 @@ async def read_content(
272276
logger.debug("Processing image")
273277
img = PILImage.open(io.BytesIO(response.content))
274278
img_bytes = optimize_image(img, content_length)
279+
logger.info(
280+
f"MCP tool response: tool=read_content project={active_project.name} "
281+
f"path={url} type=image content_type=image/jpeg"
282+
)
275283

276284
return {
277285
"type": "image",
@@ -291,6 +299,10 @@ async def read_content(
291299
"type": "error",
292300
"error": f"Document size {content_length} bytes exceeds maximum allowed size",
293301
}
302+
logger.info(
303+
f"MCP tool response: tool=read_content project={active_project.name} "
304+
f"path={url} type=document content_type={content_type}"
305+
)
294306
return {
295307
"type": "document",
296308
"source": {

src/basic_memory/mcp/tools/search.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -634,7 +634,13 @@ async def search_notes(
634634
if not search_query.entity_types:
635635
search_query.entity_types = [SearchItemType("entity")]
636636

637-
logger.debug(f"Searching for {search_query} in project {active_project.name}")
637+
logger.debug(
638+
f"Search request: project={active_project.name} "
639+
f"search_type={effective_search_type} "
640+
f"query={effective_query or '<filters-only>'} "
641+
f"note_types={len(note_types)} entity_types={len(search_query.entity_types or [])} "
642+
f"page={page} page_size={page_size}"
643+
)
638644
# Import here to avoid circular import (tools → clients → utils → tools)
639645
from basic_memory.mcp.clients import SearchClient
640646

@@ -645,6 +651,11 @@ async def search_notes(
645651
page=page,
646652
page_size=page_size,
647653
)
654+
logger.debug(
655+
f"Search response: project={active_project.name} "
656+
f"results={len(result.results)} has_more={str(result.has_more).lower()} "
657+
f"page={result.current_page} page_size={result.page_size}"
658+
)
648659

649660
# Check if we got no results and provide helpful guidance
650661
if not result.results:

tests/mcp/test_project_context.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,49 @@ def test_matches_case_insensitive_via_permalink(self, config_manager):
383383
assert result == "My Research"
384384

385385

386+
@pytest.mark.asyncio
387+
async def test_resolve_project_and_path_skips_unknown_prefix_when_project_already_fixed(
388+
config_manager, monkeypatch
389+
):
390+
from basic_memory.mcp.project_context import ProjectItem, resolve_project_and_path
391+
392+
config = config_manager.load_config()
393+
config.permalinks_include_project = True
394+
config_manager.save_config(config)
395+
396+
async def fake_get_active_project(client, project=None, context=None, headers=None):
397+
return ProjectItem(
398+
id=1,
399+
external_id="project-123",
400+
name="test-project",
401+
path="/tmp/test-project",
402+
is_default=True,
403+
)
404+
405+
async def fail_if_called(*args, **kwargs): # pragma: no cover
406+
raise AssertionError("Project resolution should not run for a plain directory prefix")
407+
408+
monkeypatch.setattr(
409+
"basic_memory.mcp.project_context.get_active_project",
410+
fake_get_active_project,
411+
)
412+
monkeypatch.setattr(
413+
"basic_memory.mcp.tools.utils.call_post",
414+
fail_if_called,
415+
)
416+
417+
active_project, resolved_path, is_memory_url = await resolve_project_and_path(
418+
client=object(),
419+
identifier="memory://testing/note",
420+
project="test-project",
421+
context=None,
422+
)
423+
424+
assert active_project.name == "test-project"
425+
assert resolved_path == "test-project/testing/note"
426+
assert is_memory_url is True
427+
428+
386429
class TestGetProjectClientRoutingOrder:
387430
"""Test that get_project_client respects explicit routing before workspace resolution."""
388431

0 commit comments

Comments
 (0)