Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/basic_memory/api/routers/resource_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def get_entity_ids(item: SearchIndexRow) -> list[int]:
from_entity = item.from_id
to_entity = item.to_id # pyright: ignore [reportReturnType]
return [from_entity, to_entity] if to_entity else [from_entity] # pyright: ignore [reportReturnType]
case _:
case _: # pragma: no cover
raise ValueError(f"Unexpected type: {item.type}")


Expand Down
64 changes: 41 additions & 23 deletions src/basic_memory/mcp/tools/notes.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,14 @@


@mcp.tool(
description="Create or update a markdown note. Returns the permalink for referencing.",
description="Create or update a markdown note. Returns a markdown formatted summary of the semantic content.",
)
async def write_note(
title: str,
content: str,
folder: str,
tags: Optional[List[str]] = None,
verbose: bool = False,
) -> EntityResponse | str:
) -> str:
"""Write a markdown note to the knowledge base.

The content can include semantic observations and relations using markdown syntax.
Expand Down Expand Up @@ -53,14 +52,16 @@ async def write_note(
content: Markdown content for the note, can include observations and relations
folder: the folder where the file should be saved
tags: Optional list of tags to categorize the note
verbose: If True, returns full EntityResponse with semantic info

Returns:
If verbose=False: Permalink that can be used to reference the note
If verbose=True: EntityResponse with full semantic details
A markdown formatted summary of the semantic content, including:
- Creation/update status
- File path and checksum
- Observation counts by category
- Relation counts (resolved/unresolved)
- Tags if present

Examples:
# Note with both explicit and inline relations
write_note(
title="Search Implementation",
content="# Search Component\\n\\n"
Expand All @@ -73,20 +74,6 @@ async def write_note(
"- depends_on [[Database Schema]]",
folder="docs/components"
)

# Note with tags
write_note(
title="Error Handling Design",
content="# Error Handling\\n\\n"
"This design builds on [[Reliability Design]].\\n\\n"
"## Approach\\n"
"- [design] Use error codes #architecture\\n"
"- [tech] Implement retry logic #implementation\\n\\n"
"## Relations\\n"
"- extends [[Base Error Handling]]",
folder="docs/design",
tags=["architecture", "reliability"]
)
"""
logger.info(f"Writing note folder:'{folder}' title: '{title}'")

Expand All @@ -101,12 +88,43 @@ async def write_note(
entity_metadata=metadata,
)

# Use existing knowledge tool
# Create or update via knowledge API
logger.info(f"Creating {entity.permalink}")
url = f"/knowledge/entities/{entity.permalink}"
response = await call_put(client, url, json=entity.model_dump())
result = EntityResponse.model_validate(response.json())
return result if verbose else result.permalink

# Format semantic summary based on status code
action = "Created" if response.status_code == 201 else "Updated"
assert result.checksum is not None
summary = [
f"# {action} {result.file_path} ({result.checksum[:8]})",
f"permalink: {result.permalink}",
]

if result.observations:
categories = {}
for obs in result.observations:
categories[obs.category] = categories.get(obs.category, 0) + 1

summary.append("\n## Observations")
for category, count in sorted(categories.items()):
summary.append(f"- {category}: {count}")

if result.relations:
unresolved = sum(1 for r in result.relations if not r.to_id)
resolved = len(result.relations) - unresolved

summary.append("\n## Relations")
summary.append(f"- Resolved: {resolved}")
if unresolved:
summary.append(f"- Unresolved: {unresolved}")
summary.append("\nUnresolved relations will be retried on next sync.")

if tags:
summary.append(f"\n## Tags\n- {', '.join(tags)}")

return "\n".join(summary)


@mcp.tool(description="Read note content by title, permalink, relation, or pattern")
Expand Down
1 change: 0 additions & 1 deletion tests/api/test_resource_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,6 @@ async def test_get_resource_entities(client, test_config, entity_repository):
entity2 = EntityResponse(**entity_response)

assert len(entity2.relations) == 1
relation = entity2.relations[0]

# Test getting the content via the relation
response = await client.get("/resource/test/*")
Expand Down
45 changes: 0 additions & 45 deletions tests/mcp/test_tool_get_entity.py

This file was deleted.

21 changes: 11 additions & 10 deletions tests/mcp/test_tool_knowledge.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
async def test_get_single_entity(client):
"""Test retrieving a single entity."""
# First create an entity
permalink = await notes.write_note(
result = await notes.write_note(
title="Test Note",
folder="test",
content="""
Expand All @@ -22,9 +22,10 @@ async def test_get_single_entity(client):
""",
tags=["test", "documentation"],
)
assert result

# Get the entity
entity = await get_entity(permalink)
entity = await get_entity("test/test-note")

# Verify entity details
assert entity.title == "Test Note"
Expand All @@ -36,48 +37,48 @@ async def test_get_single_entity(client):
async def test_get_multiple_entities(client):
"""Test retrieving multiple entities."""
# Create two test entities
permalink1 = await notes.write_note(
await notes.write_note(
title="Test Note 1",
folder="test",
content="# Test 1",
)
permalink2 = await notes.write_note(
await notes.write_note(
title="Test Note 2",
folder="test",
content="# Test 2",
)

# Get both entities
request = GetEntitiesRequest(permalinks=[permalink1, permalink2])
request = GetEntitiesRequest(permalinks=["test/test-note-1", "test/test-note-2"])
response = await get_entities(request)

# Verify we got both entities
assert len(response.entities) == 2
permalinks = {e.permalink for e in response.entities}
assert permalink1 in permalinks
assert permalink2 in permalinks
assert "test/test-note-1" in permalinks
assert "test/test-note-2" in permalinks


@pytest.mark.asyncio
async def test_delete_entities(client):
"""Test deleting entities."""
# Create a test entity
permalink = await notes.write_note(
await notes.write_note(
title="Test Note",
folder="test",
content="# Test Note to Delete",
)

# Delete the entity
request = DeleteEntitiesRequest(permalinks=[permalink])
request = DeleteEntitiesRequest(permalinks=["test/test-note"])
response = await delete_entities(request)

# Verify deletion
assert response.deleted is True

# Verify entity no longer exists
with pytest.raises(ToolError):
await get_entity(permalink)
await get_entity("test/test-note")


@pytest.mark.asyncio
Expand Down
Loading