diff --git a/src/basic_memory/mcp/server.py b/src/basic_memory/mcp/server.py index 4cfd6dba..548610c2 100644 --- a/src/basic_memory/mcp/server.py +++ b/src/basic_memory/mcp/server.py @@ -1,7 +1,7 @@ """Basic Memory MCP server using fastmcp - proxies to FastAPI endpoints.""" import sys -from typing import Any, List +from typing import Any, List, Optional, Dict from fastmcp import FastMCP from loguru import logger @@ -54,8 +54,24 @@ async def log_api_call(method: str, url: str, data: Any, response: Any): @mcp.tool() -async def create_entities(entities: list[dict]) -> dict: - """Create new entities in the knowledge graph.""" +async def create_entities(entities: List[dict]) -> dict: + """Create new entities in the knowledge graph. + + Args: + entities: List of entity dictionaries, each containing: + - name: Entity name + - entity_type: Classification (e.g., 'component', 'specification') + - description: Optional description + - observations: Optional list of initial observations + + Example: + create_entities([{ + "name": "Knowledge Format", + "entity_type": "specification", + "description": "Document format specification", + "observations": ["Uses markdown format", "Supports YAML frontmatter"] + }]) + """ url = "/knowledge/entities" data = {"entities": entities} response = await client.post(url, json=data) @@ -63,8 +79,24 @@ async def create_entities(entities: list[dict]) -> dict: @mcp.tool() -async def create_relations(relations: list[dict]) -> dict: - """Create relations between entities.""" +async def create_relations(relations: List[dict]) -> dict: + """Create relations between existing entities. + + Args: + relations: List of relation dictionaries, each containing: + - from_id: Source entity path_id + - to_id: Target entity path_id + - relation_type: Type of relationship in active voice + - context: Optional context for the relation + + Example: + create_relations([{ + "from_id": "test/parser_test", + "to_id": "component/parser", + "relation_type": "validates", + "context": "Unit test coverage" + }]) + """ url = "/knowledge/relations" data = {"relations": relations} response = await client.post(url, json=data) @@ -72,10 +104,32 @@ async def create_relations(relations: list[dict]) -> dict: @mcp.tool() -async def add_observations(path_id: str, observations: list[str]) -> dict: - """Add observations to an entity.""" +async def add_observations(path_id: str, observations: List[dict], context: Optional[str] = None) -> dict: + """Add observations to an entity. + + Args: + path_id: Entity path ID + observations: List of observations, each containing: + - content: The observation text + - category: Optional category ('tech', 'design', 'feature', 'note', 'issue', 'todo') + context: Optional shared context for all observations + + Example: + add_observations( + "specification/knowledge_format", + observations=[ + {"content": "Uses markdown format", "category": "tech"}, + {"content": "Designed for readability", "category": "design"} + ], + context="Initial design" + ) + """ url = "/knowledge/observations" - data = {"path_id": path_id, "observations": observations} + data = { + "path_id": path_id, + "observations": observations, + "context": context + } response = await client.post(url, json=data) await log_api_call("POST", url, data, response) return response.json() @@ -86,24 +140,59 @@ async def add_observations(path_id: str, observations: list[str]) -> dict: @mcp.tool() async def get_entity(path_id: str) -> dict: - """Get a specific entity by path_id.""" + """Get a specific entity by its path_id. + + Args: + path_id: Entity path ID (e.g., 'component/memory_service') + + Returns: + Complete entity information including observations and relations. + + Example: + get_entity("specification/knowledge_format") + """ url = f"/knowledge/entities/{path_id}" response = await client.get(url) return response.json() @mcp.tool() -async def search_nodes(query: str) -> dict: - """Search for entities in the knowledge graph.""" +async def search_nodes(query: str, category: Optional[str] = None) -> dict: + """Search for entities in the knowledge graph. + + Args: + query: Search text to match against entities + category: Optional category to filter observations by + + Returns: + Matching entities with their observations and relations. + + Example: + search_nodes("markdown format", category="tech") # Find tech observations about markdown + search_nodes("implementation") # Search all categories + """ url = "/knowledge/search" - data = {"query": query} + data = {"query": query, "category": category} response = await client.post(url, json=data) return response.json() @mcp.tool() async def open_nodes(path_ids: List[str]) -> dict: - """Search for entities in the knowledge graph.""" + """Load multiple entities by their path_ids. + + Args: + path_ids: List of entity path IDs to load + + Returns: + Dictionary of loaded entities with their observations and relations. + + Example: + open_nodes([ + "specification/knowledge_format", + "component/parser" + ]) + """ url = "/knowledge/nodes" data = {"path_ids": path_ids} response = await client.post(url, json=data) @@ -115,7 +204,14 @@ async def open_nodes(path_ids: List[str]) -> dict: @mcp.tool() async def delete_entities(path_ids: List[str]) -> dict: - """Search for entities in the knowledge graph.""" + """Delete entities from the knowledge graph. + + Args: + path_ids: List of entity path IDs to delete + + Example: + delete_entities(["test/obsolete_test", "component/old_component"]) + """ url = "/knowledge/entities/delete" data = {"path_ids": path_ids} response = await client.post(url, json=data) @@ -123,8 +219,19 @@ async def delete_entities(path_ids: List[str]) -> dict: @mcp.tool() -async def delete_observations(path_id: str, observations: list[str]) -> dict: - """Delete observations from an entity.""" +async def delete_observations(path_id: str, observations: List[str]) -> dict: + """Delete specific observations from an entity. + + Args: + path_id: Entity path ID + observations: List of observation content strings to delete + + Example: + delete_observations( + "component/parser", + ["Obsolete implementation detail", "Old design note"] + ) + """ url = "/knowledge/observations/delete" data = { "path_id": path_id, @@ -132,18 +239,32 @@ async def delete_observations(path_id: str, observations: list[str]) -> dict: } response = await client.post( url, json=data - ) + ) return response.json() @mcp.tool() -async def delete_relations(relations: list[dict]) -> dict: - """Delete relations between entities.""" +async def delete_relations(relations: List[dict]) -> dict: + """Delete relations between entities. + + Args: + relations: List of relation dictionaries to delete, each containing: + - from_id: Source entity path_id + - to_id: Target entity path_id + - relation_type: Type of relationship + + Example: + delete_relations([{ + "from_id": "test/old_test", + "to_id": "component/parser", + "relation_type": "validates" + }]) + """ url = "/knowledge/relations/delete" data = {"relations": relations} response = await client.post( url, json=data - ) + ) return response.json() @@ -151,8 +272,21 @@ async def delete_relations(relations: list[dict]) -> dict: @mcp.tool() -async def create_document(path: str, content: str, metadata: dict = None) -> dict: - """Create a new document.""" +async def create_document(path: str, content: str, metadata: Optional[Dict] = None) -> dict: + """Create a new document. + + Args: + path: Document path (must end in .md) + content: Document content as markdown text + metadata: Optional metadata dictionary + + Example: + create_document( + "docs/format.md", + "# Format Specification\n\nDetails here...", + metadata={"author": "AI team"} + ) + """ url = "/documents/create" data = {"path": path, "content": content, "metadata": metadata} response = await client.post(url, json=data) @@ -161,15 +295,35 @@ async def create_document(path: str, content: str, metadata: dict = None) -> dic @mcp.tool() async def get_document(path: str) -> dict: - """Get a document by path_id.""" + """Get a document by its path. + + Args: + path: Document path to retrieve + + Example: + get_document("docs/format.md") + """ url = f"/documents/{path}" response = await client.get(url) return response.json() @mcp.tool() -async def update_document(path: str, content: str, metadata: dict = None) -> dict: - """Update an existing document.""" +async def update_document(path: str, content: str, metadata: Optional[Dict] = None) -> dict: + """Update an existing document. + + Args: + path: Document path + content: New document content + metadata: Optional new metadata + + Example: + update_document( + "docs/format.md", + "# Updated Format\n\nNew content...", + metadata={"updated_by": "AI team"} + ) + """ url = f"/documents/{path}" data = {"path": path, "content": content, "metadata": metadata} response = await client.put(url, json=data) @@ -178,7 +332,14 @@ async def update_document(path: str, content: str, metadata: dict = None) -> dic @mcp.tool() async def list_documents() -> list: - """List all documents.""" + """List all documents in the system. + + Returns: + List of document paths and metadata. + + Example: + list_documents() # Get all document paths + """ url = "/documents/list" response = await client.get(url) return response.json() @@ -186,13 +347,19 @@ async def list_documents() -> list: @mcp.tool() async def delete_document(path: str) -> dict: - """Delete an existing document.""" + """Delete a document. + + Args: + path: Path of document to delete + + Example: + delete_document("docs/obsolete.md") + """ url = f"/documents/{path}" response = await client.delete(url) if response.status_code == 204: return {"deleted": True} - if __name__ == "__main__": setup_logging() logger.info("Starting Basic Memory MCP server") diff --git a/tests/mcp/test_add_observations.py b/tests/mcp/test_add_observations.py deleted file mode 100644 index 83c60434..00000000 --- a/tests/mcp/test_add_observations.py +++ /dev/null @@ -1,40 +0,0 @@ -# """Tests for the MCP server implementation using FastAPI TestClient.""" -# -# import pytest -# from mcp.types import EmbeddedResource -# -# from basic_memory.mcp.server import handle_call_tool -# from basic_memory.schemas import CreateEntityResponse, EntityResponse -# -# -# @pytest.mark.asyncio -# async def test_add_observations(app, test_entity_data, client): -# """Test adding observations to an existing entity.""" -# -# # First create an entity -# create_result = await handle_call_tool("create_entities", test_entity_data) -# create_response = CreateEntityResponse.model_validate_json(create_result[0].resource.text) # pyright: ignore [reportAttributeAccessIssue] -# entity_id = create_response.entities[0].id -# -# # Add new observation -# result = await handle_call_tool( -# "add_observations", {"entity_id": entity_id, "observations": ["A new observation"]} -# ) -# -# # Verify response format -# assert len(result) == 1 -# assert isinstance(result[0], EmbeddedResource) -# assert result[0].type == "resource" -# -# # Verify observation was added -# response = EntityResponse.model_validate_json(result[0].resource.text) # pyright: ignore [reportAttributeAccessIssue] -# assert response.id == entity_id -# assert len(response.observations) == 2 # 1 already present -# assert response.observations[1].content == "A new observation" -# -# # Verify through API -# api_response = await client.get(f"/knowledge/entities/{entity_id}") -# assert api_response.status_code == 200 -# entity = api_response.json() -# assert len(entity["observations"]) == 2 # Original + new -# assert "A new observation" in [o["content"] for o in entity["observations"]] diff --git a/tests/mcp/test_create_entitites.py b/tests/mcp/test_create_entitites.py deleted file mode 100644 index b0536f96..00000000 --- a/tests/mcp/test_create_entitites.py +++ /dev/null @@ -1,107 +0,0 @@ -# """Tests for the MCP server implementation using FastAPI TestClient.""" -# -# import pytest -# from mcp.types import EmbeddedResource -# -# from basic_memory.mcp.server import MIME_TYPE, handle_call_tool -# from basic_memory.schemas import CreateEntityResponse, SearchNodesResponse -# -# -# @pytest.mark.asyncio -# async def test_create_single_entity(app): -# """Test creating a single entity.""" -# entity_data = { -# "entities": [ -# {"name": "SingleTest", "entity_type": "test", "observations": ["Test observation"]} -# ] -# } -# -# result = await handle_call_tool("create_entities", entity_data) -# -# # Verify response format -# assert len(result) == 1 -# assert isinstance(result[0], EmbeddedResource) -# assert result[0].type == "resource" -# assert result[0].resource.mimeType == MIME_TYPE -# -# # Verify entity creation -# response = CreateEntityResponse.model_validate_json(result[0].resource.text) # pyright: ignore [reportAttributeAccessIssue] -# assert len(response.entities) == 1 -# entity = response.entities[0] -# assert entity.name == "SingleTest" -# assert entity.entity_type == "test" -# assert len(entity.observations) == 1 -# assert entity.observations[0].content == "Test observation" -# assert entity.id is not None -# -# # Verify entity can be found via search -# search_result = await handle_call_tool("search_nodes", {"query": "SingleTest"}) -# search_response = SearchNodesResponse.model_validate_json(search_result[0].resource.text) # pyright: ignore [reportAttributeAccessIssue] -# assert len(search_response.matches) == 1 -# assert search_response.matches[0].name == "SingleTest" -# -# -# @pytest.mark.asyncio -# async def test_create_multiple_entities(app): -# """Test creating multiple entities in one call.""" -# entity_data = { -# "entities": [ -# {"name": "BulkTest1", "entity_type": "test", "observations": ["First bulk test"]}, -# {"name": "BulkTest2", "entity_type": "test", "observations": ["Second bulk test"]}, -# {"name": "BulkTest3", "entity_type": "demo", "observations": ["Third bulk test"]}, -# ] -# } -# -# result = await handle_call_tool("create_entities", entity_data) -# -# # Verify response -# assert len(result) == 1 -# response = CreateEntityResponse.model_validate_json(result[0].resource.text) # pyright: ignore [reportAttributeAccessIssue] -# -# # Verify all entities were created -# assert len(response.entities) == 3 -# -# # Check specific entities -# entities = {e.name: e for e in response.entities} -# assert "BulkTest1" in entities -# assert "BulkTest2" in entities -# assert "BulkTest3" in entities -# -# # Verify IDs were generated correctly -# assert entities["BulkTest1"].id is not None -# assert entities["BulkTest2"].id is not None -# assert entities["BulkTest3"].id is not None -# -# # Verify observations were saved -# assert len(entities["BulkTest1"].observations) == 1 -# assert entities["BulkTest1"].observations[0].content == "First bulk test" -# -# # Verify entities can be found via search -# search_result = await handle_call_tool("search_nodes", {"query": "BulkTest"}) -# search_response = SearchNodesResponse.model_validate_json(search_result[0].resource.text) # pyright: ignore [reportAttributeAccessIssue] -# assert len(search_response.matches) == 3 -# -# -# @pytest.mark.asyncio -# async def test_create_entity_with_all_fields(app): -# """Test creating entity with all possible fields populated.""" -# entity_data = { -# "entities": [ -# { -# "name": "FullEntity", -# "entity_type": "test", -# "description": "A complete test entity", -# "observations": ["First observation", "Second observation"], -# } -# ] -# } -# -# result = await handle_call_tool("create_entities", entity_data) -# response = CreateEntityResponse.model_validate_json(result[0].resource.text) # pyright: ignore [reportAttributeAccessIssue] -# -# entity = response.entities[0] -# assert entity.name == "FullEntity" -# assert entity.description == "A complete test entity" -# assert len(entity.observations) == 2 -# assert entity.observations[0].content == "First observation" -# assert entity.observations[1].content == "Second observation" diff --git a/tests/mcp/test_create_relations.py b/tests/mcp/test_create_relations.py deleted file mode 100644 index 341c275e..00000000 --- a/tests/mcp/test_create_relations.py +++ /dev/null @@ -1,48 +0,0 @@ -# """Tests for the MCP server implementation using FastAPI TestClient.""" -# -# import pytest -# -# from basic_memory.mcp.server import handle_call_tool -# from basic_memory.schemas import SearchNodesResponse, CreateEntityResponse -# -# -# @pytest.mark.asyncio -# async def test_create_relations(app): -# """Test creating relations between entities.""" -# # Create two test entities -# entity_data = { -# "entities": [ -# {"name": "TestEntityA", "entity_type": "test", "observations": ["Entity A"]}, -# {"name": "TestEntityB", "entity_type": "test", "observations": ["Entity B"]}, -# ] -# } -# -# create_entity_result = await handle_call_tool("create_entities", entity_data) -# create_entity_response = CreateEntityResponse.model_validate_json( -# create_entity_result[0].resource.text # pyright: ignore [reportAttributeAccessIssue] -# ) -# -# from_entity = create_entity_response.entities[0] -# to_entity = create_entity_response.entities[1] -# # Create relation between them -# relation_data = { -# "relations": [ -# { -# "from_id": from_entity.id, -# "to_id": to_entity.id, -# "relation_type": "relates_to", -# } -# ] -# } -# -# result = await handle_call_tool("create_relations", relation_data) -# -# # Verify through search -# search_result = await handle_call_tool("search_nodes", {"query": "TestEntityA"}) -# response = SearchNodesResponse.model_validate_json(search_result[0].resource.text) # pyright: ignore [reportAttributeAccessIssue] -# -# assert len(response.matches) == 1 -# entity = response.matches[0] -# assert len(entity.relations) == 1 -# assert entity.relations[0].to_id == to_entity.id -# assert entity.relations[0].relation_type == "relates_to" diff --git a/tests/mcp/test_delete_entities.py b/tests/mcp/test_delete_entities.py deleted file mode 100644 index 3eb094f1..00000000 --- a/tests/mcp/test_delete_entities.py +++ /dev/null @@ -1,35 +0,0 @@ -# """Tests for MCP delete_entities tool.""" -# -# import pytest -# -# from basic_memory.mcp.server import handle_call_tool -# from basic_memory.schemas import SearchNodesResponse, CreateEntityResponse -# -# -# @pytest.mark.asyncio -# async def test_delete_entities(app): -# """Test deleting entities.""" -# # Create test entities -# entities = { -# "entities": [ -# {"name": "DeleteTest1", "entity_type": "test", "observations": ["To be deleted 1"]}, -# {"name": "DeleteTest2", "entity_type": "test", "observations": ["To be deleted 2"]}, -# ] -# } -# create_entity_result = await handle_call_tool("create_entities", entities) -# create_entity_response = CreateEntityResponse.model_validate_json( -# create_entity_result[0].resource.text # pyright: ignore [reportAttributeAccessIssue] -# ) -# -# # Delete first entity -# await handle_call_tool( -# "delete_entities", {"entity_ids": [create_entity_response.entities[0].id]} -# ) -# -# # Verify through search -# search_result = await handle_call_tool("search_nodes", {"query": "DeleteTest"}) -# search_response = SearchNodesResponse.model_validate_json(search_result[0].resource.text) # pyright: ignore [reportAttributeAccessIssue] -# -# # Only second entity should remain -# assert len(search_response.matches) == 1 -# assert search_response.matches[0].name == "DeleteTest2" diff --git a/tests/mcp/test_delete_observations.py b/tests/mcp/test_delete_observations.py deleted file mode 100644 index 341721e4..00000000 --- a/tests/mcp/test_delete_observations.py +++ /dev/null @@ -1,47 +0,0 @@ -# """Tests for MCP delete_observations tool.""" -# -# import pytest -# -# from basic_memory.mcp.server import handle_call_tool -# from basic_memory.schemas import CreateEntityResponse, SearchNodesResponse -# -# -# @pytest.mark.asyncio -# async def test_delete_observations(app): -# """Test deleting specific observations from an entity.""" -# # Create entity with multiple observations -# entity_data = { -# "entities": [ -# { -# "name": "ObsDeleteTest", -# "entity_type": "test", -# "observations": [ -# "Keep this observation", -# "Delete this observation", -# "Also keep this", -# ], -# } -# ] -# } -# create_result = await handle_call_tool("create_entities", entity_data) -# create_response = CreateEntityResponse.model_validate_json(create_result[0].resource.text) # pyright: ignore [reportAttributeAccessIssue] -# entity_id = create_response.entities[0].id -# -# # Delete specific observation -# await handle_call_tool( -# "delete_observations", {"entity_id": entity_id, "deletions": ["Delete this observation"]} -# ) -# -# # Verify through search -# search_result = await handle_call_tool("search_nodes", {"query": "ObsDeleteTest"}) -# search_response = SearchNodesResponse.model_validate_json( -# search_result[0].resource.text # pyright: ignore [reportAttributeAccessIssue] -# ) -# -# # Check remaining observations -# entity = search_response.matches[0] -# observations = [o.content for o in entity.observations] -# assert len(observations) == 2 -# assert "Delete this observation" not in observations -# assert "Keep this observation" in observations -# assert "Also keep this" in observations diff --git a/tests/mcp/test_delete_relations.py b/tests/mcp/test_delete_relations.py deleted file mode 100644 index aca41988..00000000 --- a/tests/mcp/test_delete_relations.py +++ /dev/null @@ -1,58 +0,0 @@ -# """Tests for MCP delete_relations tool.""" -# -# import pytest -# -# from basic_memory.mcp.server import handle_call_tool -# from basic_memory.schemas import SearchNodesResponse, CreateEntityResponse -# -# -# @pytest.mark.asyncio -# async def test_delete_relations(app): -# """Test deleting relations between entities.""" -# # Create test entities with relation -# entities = { -# "entities": [ -# {"name": "RelSource", "entity_type": "test", "observations": ["Source entity"]}, -# {"name": "RelTarget", "entity_type": "test", "observations": ["Target entity"]}, -# ] -# } -# create_entity_result = await handle_call_tool("create_entities", entities) -# create_entity_response = CreateEntityResponse.model_validate_json( -# create_entity_result[0].resource.text # pyright: ignore [reportAttributeAccessIssue] -# ) -# from_entity = create_entity_response.entities[0] -# to_entity = create_entity_response.entities[1] -# -# # Create relation -# relation = { -# "relations": [ -# { -# "from_id": from_entity.id, -# "to_id": to_entity.id, -# "relation_type": "relates_to", -# } -# ] -# } -# await handle_call_tool("create_relations", relation) -# -# # Delete the relation -# await handle_call_tool( -# "delete_relations", -# { -# "relations": [ -# { -# "from_id": from_entity.id, -# "to_id": to_entity.id, -# "relation_type": "relates_to", -# } -# ] -# }, -# ) -# -# # Verify through search -# search_result = await handle_call_tool("search_nodes", {"query": "relsource"}) -# search_response = SearchNodesResponse.model_validate_json(search_result[0].resource.text) # pyright: ignore [reportAttributeAccessIssue] -# -# # Source entity should exist but have no relations -# assert len(search_response.matches) == 1 -# assert len(search_response.matches[0].relations) == 0 diff --git a/tests/mcp/test_mcp_server.py b/tests/mcp/test_mcp_server.py deleted file mode 100644 index b43e42ea..00000000 --- a/tests/mcp/test_mcp_server.py +++ /dev/null @@ -1,242 +0,0 @@ -# """Tests for the MCP server implementation using FastAPI TestClient.""" -# -# import pytest -# from mcp.shared.exceptions import McpError -# from mcp.types import INVALID_PARAMS, METHOD_NOT_FOUND -# -# from basic_memory.mcp.server import handle_call_tool -# -# -# @pytest.mark.asyncio -# async def test_invalid_tool_name(app): -# """Test calling a non-existent tool.""" -# with pytest.raises(McpError) as exc: -# await handle_call_tool("not_a_tool", {}) -# assert "Unknown tool" in str(exc.value) -# -# -# @pytest.mark.asyncio -# async def test_missing_required_field(app): -# """Test validation when required fields are missing.""" -# with pytest.raises(McpError) as exc: -# await handle_call_tool("search_nodes", {}) -# assert "query" in str(exc.value).lower() -# assert exc.value.args[0] == INVALID_PARAMS -# -# with pytest.raises(McpError) as exc: -# await handle_call_tool("create_entities", {}) -# assert "entities" in str(exc.value).lower() -# assert exc.value.args[0] == INVALID_PARAMS -# -# with pytest.raises(McpError) as exc: -# await handle_call_tool("create_document", {}) -# assert exc.value.args[0] == INVALID_PARAMS -# error_msg = str(exc.value).lower() -# assert "path" in error_msg or "content" in error_msg -# -# -# @pytest.mark.asyncio -# async def test_empty_arrays(app): -# """Test validation of array fields that can't be empty.""" -# with pytest.raises(McpError) as exc: -# await handle_call_tool("create_entities", {"entities": []}) -# assert INVALID_PARAMS == exc.value.args[0] -# -# with pytest.raises(McpError) as exc: -# await handle_call_tool("open_nodes", {"entity_ids": []}) -# assert INVALID_PARAMS == exc.value.args[0] -# -# -# @pytest.mark.asyncio -# async def test_invalid_field_types(app): -# """Test validation when fields have wrong types.""" -# with pytest.raises(McpError) as exc: -# await handle_call_tool("search_nodes", {"query": 123}) -# assert "string" in str(exc.value).lower() -# assert exc.value.args[0] == INVALID_PARAMS -# -# with pytest.raises(McpError) as exc: -# await handle_call_tool("create_entities", {"entities": "not an array"}) -# assert "array" in str(exc.value).lower() or "list" in str(exc.value).lower() -# assert exc.value.args[0] == INVALID_PARAMS -# -# with pytest.raises(McpError) as exc: -# await handle_call_tool("get_document", {"id": "not an integer"}) -# assert "integer" in str(exc.value).lower() -# assert exc.value.args[0] == INVALID_PARAMS -# -# -# @pytest.mark.asyncio -# async def test_invalid_nested_fields(app): -# """Test validation of nested object fields.""" -# with pytest.raises(McpError) as exc: -# await handle_call_tool( -# "create_entities", -# { -# "entities": [ -# { -# "name": "Test", -# # Missing required entity_type -# "observations": [], -# } -# ] -# }, -# ) -# assert "entity_type" in str(exc.value).lower() -# assert exc.value.args[0] == INVALID_PARAMS -# -# with pytest.raises(McpError) as exc: -# await handle_call_tool( -# "create_document", -# { -# "path": "test.md", -# "content": "test", -# "doc_metadata": "not an object" # Should be dict/null -# }, -# ) -# assert "doc_metadata" in str(exc.value).lower() -# assert exc.value.args[0] == INVALID_PARAMS -# -# -# @pytest.mark.asyncio -# async def test_invalid_relation_format_to_id(app): -# """Test validation of relation data.""" -# with pytest.raises(McpError) as exc: -# await handle_call_tool( -# "create_relations", -# { -# "relations": [ -# { -# "from_id": 1, -# # Missing to_id -# "relation_type": "relates_to", -# } -# ] -# }, -# ) -# assert "to_id" in str(exc.value).lower() -# -# -# @pytest.mark.asyncio -# async def test_invalid_relation_format_relation_type(app): -# # Invalid relation type -# with pytest.raises(McpError) as exc: -# await handle_call_tool( -# "create_relations", -# { -# "relations": [ -# { -# "from_id": 1, -# "to_id": 2, -# "relation_type": "", # Empty relation type -# } -# ] -# }, -# ) -# assert "relation_type" in str(exc.value).lower() -# -# -# @pytest.mark.asyncio -# async def test_observation_validation_len(app): -# """Test validation specific to observations.""" -# # Empty observations -# with pytest.raises(McpError) as exc: -# await handle_call_tool( -# "add_observations", -# { -# "entity_id": 1, -# "observations": ["", ""], # Empty observations -# }, -# ) -# assert "observations" in str(exc.value).lower() -# -# -# @pytest.mark.asyncio -# async def test_observation_validation_delete(app): -# # Empty deletions -# with pytest.raises(McpError) as exc: -# await handle_call_tool( -# "delete_observations", -# { -# "entity_id": 1, -# "deletions": [], # Empty deletions -# }, -# ) -# assert INVALID_PARAMS == exc.value.args[0] -# -# -# @pytest.mark.asyncio -# async def test_edge_case_validation_search_len(app): -# """Test edge cases in validation.""" -# # Very long strings -# with pytest.raises(McpError) as exc: -# await handle_call_tool( -# "search_nodes", -# {"query": "x" * 10000}, # Extremely long query -# ) -# -# -# @pytest.mark.asyncio -# async def test_document_endpoint_validation(app): -# """Test validation specific to document endpoints.""" -# # Invalid ID format for get_document -# with pytest.raises(McpError) as exc: -# await handle_call_tool("get_document", {"id": -1}) -# assert INVALID_PARAMS == exc.value.args[0] -# assert "greater than 0" in str(exc.value).lower() -# -# # Invalid document path -# with pytest.raises(McpError) as exc: -# await handle_call_tool( -# "create_document", -# { -# "path": "", # Empty path -# "content": "test content" -# } -# ) -# assert INVALID_PARAMS == exc.value.args[0] -# assert "path" in str(exc.value).lower() -# -# # Update without content -# with pytest.raises(McpError) as exc: -# await handle_call_tool( -# "update_document", -# { -# "id": 1, -# "doc_metadata": None -# } -# ) -# assert INVALID_PARAMS == exc.value.args[0] -# assert "content" in str(exc.value).lower() -# -# -# # We'll skip this test for now since it requires database setup -# @pytest.mark.skip(reason="Requires database setup") -# @pytest.mark.asyncio -# async def test_document_http_methods(app): -# """Test that document endpoints use correct HTTP methods.""" -# # Test GET endpoints -# await handle_call_tool("list_documents", {}) -# await handle_call_tool("get_document", {"id": 1}) -# -# # Test POST endpoint -# response = await handle_call_tool( -# "create_document", -# { -# "path": "test.md", -# "content": "test content" -# } -# ) -# -# # Test PUT endpoint -# await handle_call_tool( -# "update_document", -# { -# "id": 1, -# "content": "updated content", -# "doc_metadata": None -# } -# ) -# -# # Test DELETE endpoint -# await handle_call_tool("delete_document", {"id": 1}) diff --git a/tests/mcp/test_open_nodes.py b/tests/mcp/test_open_nodes.py deleted file mode 100644 index e81bcfe7..00000000 --- a/tests/mcp/test_open_nodes.py +++ /dev/null @@ -1,63 +0,0 @@ -# """Tests for MCP open_nodes tool.""" -# -# import pytest -# from mcp.types import EmbeddedResource -# -# from basic_memory.mcp.server import MIME_TYPE, handle_call_tool -# from basic_memory.schemas import OpenNodesResponse, CreateEntityResponse -# -# -# @pytest.mark.asyncio -# async def test_open_nodes(app): -# """Test retrieving specific nodes by name.""" -# # Create test entities -# entity_data = { -# "entities": [ -# { -# "name": "OpenTestA", -# "entity_type": "test", -# "observations": ["First test entity"], -# }, -# { -# "name": "OpenTestB", -# "entity_type": "test", -# "observations": ["Second test entity"], -# }, -# { -# "name": "OpenTestC", -# "entity_type": "test", -# "observations": ["Third test entity"], -# }, -# ] -# } -# -# create_entity_result = await handle_call_tool("create_entities", entity_data) -# create_entity_response = CreateEntityResponse.model_validate_json( -# create_entity_result[0].resource.text # pyright: ignore [reportAttributeAccessIssue] -# ) -# entity_a = create_entity_response.entities[0] -# entity_b = create_entity_response.entities[1] -# -# # Open specific nodes -# result = await handle_call_tool("open_nodes", {"entity_ids": [entity_a.id, entity_b.id]}) -# -# # Verify response format -# assert len(result) == 1 -# assert isinstance(result[0], EmbeddedResource) -# assert result[0].type == "resource" -# assert result[0].resource.mimeType == MIME_TYPE -# -# # Verify entities returned -# response = OpenNodesResponse.model_validate_json(result[0].resource.text) # pyright: ignore [reportAttributeAccessIssue] -# assert len(response.entities) == 2 -# -# # Entities should be returned in same order as requested -# assert response.entities[0].name == "OpenTestA" -# assert response.entities[1].name == "OpenTestB" -# -# # Verify entity content -# entity = response.entities[0] -# assert entity.id == entity_a.id -# assert entity.entity_type == "test" -# assert len(entity.observations) == 1 -# assert entity.observations[0].content == "First test entity" diff --git a/tests/mcp/test_search_nodes.py b/tests/mcp/test_search_nodes.py deleted file mode 100644 index 853d8f3d..00000000 --- a/tests/mcp/test_search_nodes.py +++ /dev/null @@ -1,37 +0,0 @@ -# """Tests for the MCP server implementation using FastAPI TestClient.""" -# -# import pytest -# from mcp.types import EmbeddedResource -# -# from basic_memory.mcp.server import MIME_TYPE, handle_call_tool -# from basic_memory.schemas import SearchNodesResponse -# -# -# @pytest.mark.asyncio -# async def test_search_nodes(app, test_entity_data, client): -# """Test searching for an entity after creating it.""" -# -# # First create an entity -# await handle_call_tool("create_entities", test_entity_data) -# -# # Then search for it -# result = await handle_call_tool("search_nodes", {"query": "Test Entity"}) -# -# # Verify response format -# assert len(result) == 1 -# assert isinstance(result[0], EmbeddedResource) -# assert result[0].type == "resource" -# assert result[0].resource.mimeType == MIME_TYPE -# -# # Verify search results -# response = SearchNodesResponse.model_validate_json(result[0].resource.text) # pyright: ignore [reportAttributeAccessIssue] -# assert len(response.matches) == 1 -# assert response.matches[0].name == "Test Entity" -# assert response.query == "Test Entity" -# -# # Verify through API -# api_response = await client.post("/knowledge/search", json={"query": "Test Entity"}) -# assert api_response.status_code == 200 -# data = api_response.json() -# assert len(data["matches"]) == 1 -# assert data["matches"][0]["name"] == "Test Entity" diff --git a/tests/mcp/test_tool_docs.py b/tests/mcp/test_tool_docs.py deleted file mode 100644 index e6cfca8f..00000000 --- a/tests/mcp/test_tool_docs.py +++ /dev/null @@ -1,25 +0,0 @@ -# """Test to show MCP tool documentation.""" -# -# import json -# import pytest -# from mcp.types import Tool -# from basic_memory.mcp.server import handle_list_tools -# -# -# @pytest.mark.asyncio -# async def test_list_tools(): -# """List available tools and their documentation.""" -# tools = await handle_list_tools() -# assert isinstance(tools, list) -# assert all(isinstance(t, Tool) for t in tools) -# -# print("\nAvailable MCP Tools:\n") -# -# # Print each tool's documentation -# for tool in tools: -# print(f"Tool: {tool.name}") -# print(f"Description: {tool.description}") -# print("Required fields:", tool.inputSchema.get("required", [])) -# print() -# print("Schema:", json.dumps(tool.inputSchema, indent=2)) -# print("-" * 80 + "\n")