diff --git a/servers/mcp-neo4j-memory/CHANGELOG.md b/servers/mcp-neo4j-memory/CHANGELOG.md index 955ff59..74fe86d 100644 --- a/servers/mcp-neo4j-memory/CHANGELOG.md +++ b/servers/mcp-neo4j-memory/CHANGELOG.md @@ -3,6 +3,7 @@ ### Fixed ### Changed +* Updated tool docstrings to better describe their function, inputs and outputs ### Added diff --git a/servers/mcp-neo4j-memory/src/mcp_neo4j_memory/neo4j_memory.py b/servers/mcp-neo4j-memory/src/mcp_neo4j_memory/neo4j_memory.py index a6aa217..b47bb11 100644 --- a/servers/mcp-neo4j-memory/src/mcp_neo4j_memory/neo4j_memory.py +++ b/servers/mcp-neo4j-memory/src/mcp_neo4j_memory/neo4j_memory.py @@ -2,7 +2,7 @@ from typing import Any, Dict, List from neo4j import AsyncDriver, RoutingControl -from pydantic import BaseModel +from pydantic import BaseModel, Field # Set up logging @@ -11,26 +11,104 @@ # Models for our knowledge graph class Entity(BaseModel): - name: str - type: str - observations: List[str] + """Represents a memory entity in the knowledge graph. + + Example: + { + "name": "John Smith", + "type": "person", + "observations": ["Works at Neo4j", "Lives in San Francisco", "Expert in graph databases"] + } + """ + name: str = Field( + description="Unique identifier/name for the entity. Should be descriptive and specific.", + min_length=1, + examples=["John Smith", "Neo4j Inc", "San Francisco"] + ) + type: str = Field( + description="Category or classification of the entity. Common types: 'person', 'company', 'location', 'concept', 'event'", + min_length=1, + examples=["person", "company", "location", "concept", "event"] + ) + observations: List[str] = Field( + description="List of facts, observations, or notes about this entity. Each observation should be a complete, standalone fact.", + examples=[["Works at Neo4j", "Lives in San Francisco"], ["Headquartered in Sweden", "Graph database company"]] + ) class Relation(BaseModel): - source: str - target: str - relationType: str + """Represents a relationship between two entities in the knowledge graph. + + Example: + { + "source": "John Smith", + "target": "Neo4j Inc", + "relationType": "WORKS_AT" + } + """ + source: str = Field( + description="Name of the source entity (must match an existing entity name exactly)", + min_length=1, + examples=["John Smith", "Neo4j Inc"] + ) + target: str = Field( + description="Name of the target entity (must match an existing entity name exactly)", + min_length=1, + examples=["Neo4j Inc", "San Francisco"] + ) + relationType: str = Field( + description="Type of relationship between source and target. Use descriptive, uppercase names with underscores.", + min_length=1, + examples=["WORKS_AT", "LIVES_IN", "MANAGES", "COLLABORATES_WITH", "LOCATED_IN"] + ) class KnowledgeGraph(BaseModel): - entities: List[Entity] - relations: List[Relation] + """Complete knowledge graph containing entities and their relationships.""" + entities: List[Entity] = Field( + description="List of all entities in the knowledge graph", + default=[] + ) + relations: List[Relation] = Field( + description="List of all relationships between entities", + default=[] + ) class ObservationAddition(BaseModel): - entityName: str - observations: List[str] + """Request to add new observations to an existing entity. + + Example: + { + "entityName": "John Smith", + "observations": ["Recently promoted to Senior Engineer", "Speaks fluent German"] + } + """ + entityName: str = Field( + description="Exact name of the existing entity to add observations to", + min_length=1, + examples=["John Smith", "Neo4j Inc"] + ) + observations: List[str] = Field( + description="New observations/facts to add to the entity. Each should be unique and informative.", + min_length=1 + ) class ObservationDeletion(BaseModel): - entityName: str - observations: List[str] + """Request to delete specific observations from an existing entity. + + Example: + { + "entityName": "John Smith", + "observations": ["Old job title", "Outdated contact info"] + } + """ + entityName: str = Field( + description="Exact name of the existing entity to remove observations from", + min_length=1, + examples=["John Smith", "Neo4j Inc"] + ) + observations: List[str] = Field( + description="Exact observation texts to delete from the entity (must match existing observations exactly)", + min_length=1 + ) class Neo4jMemory: def __init__(self, neo4j_driver: AsyncDriver): diff --git a/servers/mcp-neo4j-memory/src/mcp_neo4j_memory/server.py b/servers/mcp-neo4j-memory/src/mcp_neo4j_memory/server.py index 69533f5..4019f64 100644 --- a/servers/mcp-neo4j-memory/src/mcp_neo4j_memory/server.py +++ b/servers/mcp-neo4j-memory/src/mcp_neo4j_memory/server.py @@ -7,7 +7,8 @@ from fastmcp.server import FastMCP from fastmcp.exceptions import ToolError -from fastmcp.tools.tool import ToolResult, TextContent +from fastmcp.tools.tool import ToolResult +from mcp.types import TextContent from neo4j.exceptions import Neo4jError from mcp.types import ToolAnnotations @@ -28,8 +29,26 @@ def create_mcp_server(memory: Neo4jMemory) -> FastMCP: destructiveHint=False, idempotentHint=True, openWorldHint=True)) - async def read_graph() -> KnowledgeGraph: - """Read the entire knowledge graph.""" + async def read_graph() -> ToolResult: + """Read the entire knowledge graph with all entities and relationships. + + Returns the complete memory graph including all stored entities and their relationships. + Use this to get a full overview of stored knowledge. + + Returns: + KnowledgeGraph: Complete graph with all entities and relations + + Example response: + { + "entities": [ + {"name": "John Smith", "type": "person", "observations": ["Works at Neo4j"]}, + {"name": "Neo4j Inc", "type": "company", "observations": ["Graph database company"]} + ], + "relations": [ + {"source": "John Smith", "target": "Neo4j Inc", "relationType": "WORKS_AT"} + ] + } + """ logger.info("MCP tool: read_graph") try: result = await memory.read_graph() @@ -47,8 +66,32 @@ async def read_graph() -> KnowledgeGraph: destructiveHint=False, idempotentHint=True, openWorldHint=True)) - async def create_entities(entities: list[Entity] = Field(..., description="List of entities to create")) -> list[Entity]: - """Create multiple new entities in the knowledge graph.""" + async def create_entities(entities: list[Entity] = Field(..., description="List of entities to create with name, type, and observations")) -> ToolResult: + """Create multiple new entities in the knowledge graph. + + Creates new memory entities with their associated observations. If an entity with the same name + already exists, this operation will merge the observations with existing ones. + + + Returns: + list[Entity]: The created entities with their final state + + Example call: + { + "entities": [ + { + "name": "Alice Johnson", + "type": "person", + "observations": ["Software engineer", "Lives in Seattle", "Enjoys hiking"] + }, + { + "name": "Microsoft", + "type": "company", + "observations": ["Technology company", "Headquartered in Redmond, WA"] + } + ] + } + """ logger.info(f"MCP tool: create_entities ({len(entities)} entities)") try: entity_objects = [Entity.model_validate(entity) for entity in entities] @@ -67,8 +110,31 @@ async def create_entities(entities: list[Entity] = Field(..., description="List destructiveHint=False, idempotentHint=True, openWorldHint=True)) - async def create_relations(relations: list[Relation] = Field(..., description="List of relations to create")) -> list[Relation]: - """Create multiple new relations between entities.""" + async def create_relations(relations: list[Relation] = Field(..., description="List of relations to create between existing entities")) -> ToolResult: + """Create multiple new relationships between existing entities in the knowledge graph. + + Creates directed relationships between entities that already exist. Both source and target + entities must already be present in the graph. Use descriptive relationship types. + + Returns: + list[Relation]: The created relationships + + Example call: + { + "relations": [ + { + "source": "Alice Johnson", + "target": "Microsoft", + "relationType": "WORKS_AT" + }, + { + "source": "Alice Johnson", + "target": "Seattle", + "relationType": "LIVES_IN" + } + ] + } + """ logger.info(f"MCP tool: create_relations ({len(relations)} relations)") try: relation_objects = [Relation.model_validate(relation) for relation in relations] @@ -87,8 +153,29 @@ async def create_relations(relations: list[Relation] = Field(..., description="L destructiveHint=False, idempotentHint=True, openWorldHint=True)) - async def add_observations(observations: list[ObservationAddition] = Field(..., description="List of observations to add")) -> list[dict[str, str | list[str]]]: - """Add new observations to existing entities.""" + async def add_observations(observations: list[ObservationAddition] = Field(..., description="List of observations to add to existing entities")) -> ToolResult: + """Add new observations/facts to existing entities in the knowledge graph. + + Appends new observations to entities that already exist. The entity must be present + in the graph before adding observations. Each observation should be a distinct fact. + + Returns: + list[dict]: Details about the added observations including entity name and new facts + + Example call: + { + "observations": [ + { + "entityName": "Alice Johnson", + "observations": ["Promoted to Senior Engineer", "Completed AWS certification"] + }, + { + "entityName": "Microsoft", + "observations": ["Launched new AI products", "Stock price increased 15%"] + } + ] + } + """ logger.info(f"MCP tool: add_observations ({len(observations)} additions)") try: observation_objects = [ObservationAddition.model_validate(obs) for obs in observations] @@ -107,8 +194,22 @@ async def add_observations(observations: list[ObservationAddition] = Field(..., destructiveHint=True, idempotentHint=True, openWorldHint=True)) - async def delete_entities(entityNames: list[str] = Field(..., description="List of entity names to delete")) -> str: - """Delete multiple entities and their associated relations.""" + async def delete_entities(entityNames: list[str] = Field(..., description="List of exact entity names to delete permanently")) -> ToolResult: + """Delete entities and all their associated relationships from the knowledge graph. + + Permanently removes entities from the graph along with all relationships they participate in. + This is a destructive operation that cannot be undone. Entity names must match exactly. + + Returns: + str: Success confirmation message + + Example call: + { + "entityNames": ["Old Company", "Outdated Person"] + } + + Warning: This will delete the entities and ALL relationships they're involved in. + """ logger.info(f"MCP tool: delete_entities ({len(entityNames)} entities)") try: await memory.delete_entities(entityNames) @@ -126,8 +227,31 @@ async def delete_entities(entityNames: list[str] = Field(..., description="List destructiveHint=True, idempotentHint=True, openWorldHint=True)) - async def delete_observations(deletions: list[ObservationDeletion] = Field(..., description="List of observations to delete")) -> str: - """Delete specific observations from entities.""" + async def delete_observations(deletions: list[ObservationDeletion] = Field(..., description="List of specific observations to remove from entities")) -> ToolResult: + """Delete specific observations from existing entities in the knowledge graph. + + Removes specific observation texts from entities. The observation text must match exactly + what is stored. The entity will remain but the specified observations will be deleted. + + Returns: + str: Success confirmation message + + Example call: + { + "deletions": [ + { + "entityName": "Alice Johnson", + "observations": ["Old job title", "Outdated phone number"] + }, + { + "entityName": "Microsoft", + "observations": ["Former CEO information"] + } + ] + } + + Note: Observation text must match exactly (case-sensitive) to be deleted. + """ logger.info(f"MCP tool: delete_observations ({len(deletions)} deletions)") try: deletion_objects = [ObservationDeletion.model_validate(deletion) for deletion in deletions] @@ -146,8 +270,34 @@ async def delete_observations(deletions: list[ObservationDeletion] = Field(..., destructiveHint=True, idempotentHint=True, openWorldHint=True)) - async def delete_relations(relations: list[Relation] = Field(..., description="List of relations to delete")) -> str: - """Delete multiple relations from the graph.""" + async def delete_relations(relations: list[Relation] = Field(..., description="List of specific relationships to delete from the graph")) -> ToolResult: + """Delete specific relationships between entities in the knowledge graph. + + Removes relationships while keeping the entities themselves. The source, target, and + relationship type must match exactly for deletion. This only affects the relationships, + not the entities they connect. + + Returns: + str: Success confirmation message + + Example call: + { + "relations": [ + { + "source": "Alice Johnson", + "target": "Old Company", + "relationType": "WORKS_AT" + }, + { + "source": "John Smith", + "target": "Former City", + "relationType": "LIVES_IN" + } + ] + } + + Note: All fields (source, target, relationType) must match exactly for deletion. + """ logger.info(f"MCP tool: delete_relations ({len(relations)} relations)") try: relation_objects = [Relation.model_validate(relation) for relation in relations] @@ -166,8 +316,23 @@ async def delete_relations(relations: list[Relation] = Field(..., description="L destructiveHint=False, idempotentHint=True, openWorldHint=True)) - async def search_memories(query: str = Field(..., description="Search query for nodes")) -> KnowledgeGraph: - """Search for memories based on a query containing search terms.""" + async def search_memories(query: str = Field(..., description="Fulltext search query to find entities by name, type, or observations")) -> ToolResult: + """Search for entities in the knowledge graph using fulltext search. + + Searches across entity names, types, and observations using Neo4j's fulltext index. + Returns matching entities and their related connections. Supports partial matches + and multiple search terms. + + Returns: + KnowledgeGraph: Subgraph containing matching entities and their relationships + + Example call: + { + "query": "engineer software" + } + + This searches for entities containing "engineer" or "software" in their name, type, or observations. + """ logger.info(f"MCP tool: search_memories ('{query}')") try: result = await memory.search_memories(query) @@ -185,8 +350,22 @@ async def search_memories(query: str = Field(..., description="Search query for destructiveHint=False, idempotentHint=True, openWorldHint=True)) - async def find_memories_by_name(names: list[str] = Field(..., description="List of node names to find")) -> KnowledgeGraph: - """Find specific memories by name.""" + async def find_memories_by_name(names: list[str] = Field(..., description="List of exact entity names to retrieve")) -> ToolResult: + """Find specific entities by their exact names. + + Retrieves entities that exactly match the provided names, along with all their + relationships and connected entities. Use this when you know the exact entity names. + + Returns: + KnowledgeGraph: Subgraph containing the specified entities and their relationships + + Example call: + { + "names": ["Alice Johnson", "Microsoft", "Seattle"] + } + + This retrieves the entities with exactly those names plus their connections. + """ logger.info(f"MCP tool: find_memories_by_name ({len(names)} names)") try: result = await memory.find_memories_by_name(names)