MemoryServer wip

This commit is contained in:
phernandez
2024-12-08 14:14:24 -06:00
parent fb21192119
commit e0361d3422
3 changed files with 287 additions and 107 deletions
+188 -64
View File
@@ -1,77 +1,201 @@
"""MCP server implementation for basic-memory."""
from pathlib import Path
from typing import Annotated, List, Dict, Any
from typing import List, Dict, Any
from mcp.server import Server
from mcp.types import Tool, TextContent, METHOD_NOT_FOUND, INVALID_PARAMS, INTERNAL_ERROR
from mcp.shared.exceptions import McpError
from pydantic import BaseModel, Field
from basic_memory import deps
from basic_memory.schemas import EntityIn, ObservationIn, RelationIn
class MemoryServer(Server):
"""Extended server class that exposes handlers for testing."""
def __init__(self):
super().__init__("basic-memory")
self.register_handlers()
def register_handlers(self):
"""Register all handlers with proper decorators."""
@self.list_tools()
async def handle_list_tools() -> List[Tool]:
"""Define the available tools."""
return [
Tool(
name="create_entities",
description="Create multiple new entities in the knowledge graph",
inputSchema={
"type": "object",
"properties": {
"entities": {
"type": "array",
"items": EntityIn.model_json_schema(),
"minItems": 1
}
},
"required": ["entities"]
}
),
Tool(
name="search_nodes",
description="Search for nodes in the knowledge graph",
inputSchema={
"type": "object",
"properties": {
"query": {
"type": "string",
"minLength": 1
}
},
"required": ["query"]
}
),
Tool(
name="open_nodes",
description="Open specific nodes by their names",
inputSchema={
"type": "object",
"properties": {
"names": {
"type": "array",
"items": {"type": "string"},
"minItems": 1
}
},
"required": ["names"]
}
),
Tool(
name="add_observations",
description="Add observations to existing entities",
inputSchema={
"type": "object",
"properties": {
"entityId": {"type": "string"},
"observations": {
"type": "array",
"items": ObservationIn.model_json_schema(),
"minItems": 1
}
},
"required": ["entityId", "observations"]
}
),
Tool(
name="create_relations",
description="Create relations between entities",
inputSchema={
"type": "object",
"properties": {
"relations": {
"type": "array",
"items": RelationIn.model_json_schema(),
"minItems": 1
}
},
"required": ["relations"]
}
),
Tool(
name="delete_entities",
description="Delete entities from the knowledge graph",
inputSchema={
"type": "object",
"properties": {
"names": {
"type": "array",
"items": {"type": "string"},
"minItems": 1
}
},
"required": ["names"]
}
),
Tool(
name="delete_observations",
description="Delete observations from entities",
inputSchema={
"type": "object",
"properties": {
"deletions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"entityName": {"type": "string"},
"observations": {
"type": "array",
"items": {"type": "string"},
"minItems": 1
}
},
"required": ["entityName", "observations"]
}
}
},
"required": ["deletions"]
}
)
]
@self.call_tool()
async def handle_call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
"""Handle tool calls by delegating to the memory service."""
try:
# Get project path (could come from context in future)
project_path = Path.home() / ".basic-memory" / "projects" / "default"
# Get services with proper lifecycle management
async with deps.get_project_services(project_path) as memory_service:
match name:
case "create_entities":
# Each entity in arguments["entities"] will be validated by EntityIn
result = await memory_service.create_entities(arguments["entities"])
return [TextContent(type="text", text=str(result))]
case "search_nodes":
result = await memory_service.search_nodes(arguments["query"])
return [TextContent(type="text", text=str(result))]
case "open_nodes":
result = await memory_service.open_nodes(arguments["names"])
return [TextContent(type="text", text=str(result))]
case "add_observations":
result = await memory_service.add_observations(arguments)
return [TextContent(type="text", text=str(result))]
case "create_relations":
result = await memory_service.create_relations(arguments["relations"])
return [TextContent(type="text", text=str(result))]
case "delete_entities":
await memory_service.delete_entities(arguments["names"])
return [TextContent(type="text", text="Entities deleted")]
case "delete_observations":
await memory_service.delete_observations(arguments["deletions"])
return [TextContent(type="text", text="Observations deleted")]
case _:
raise McpError(
METHOD_NOT_FOUND,
f"Unknown tool: {name}"
)
except ValueError as e:
raise McpError(INVALID_PARAMS, str(e))
except Exception as e:
raise McpError(INTERNAL_ERROR, str(e))
# Store handlers as instance attributes for testing
self.handle_list_tools = handle_list_tools
self.handle_call_tool = handle_call_tool
# Create server instance
server = Server("basic-memory")
# Parameter models
class CreateEntitiesParams(BaseModel):
"""Parameters for creating entities."""
entities: Annotated[List[Dict[str, Any]], Field(
description="List of entities to create",
min_items=1
)]
class SearchNodesParams(BaseModel):
"""Parameters for searching nodes."""
query: Annotated[str, Field(
description="Search query to match against entities"
)]
@server.list_tools()
async def list_tools() -> List[Tool]:
"""Define the available tools."""
return [
Tool(
name="create_entities",
description="Create multiple new entities in the knowledge graph",
inputSchema=CreateEntitiesParams.model_json_schema()
),
Tool(
name="search_nodes",
description="Search for nodes in the knowledge graph",
inputSchema=SearchNodesParams.model_json_schema()
)
]
@server.call_tool()
async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
"""Handle tool calls by delegating to the memory service."""
try:
# Get project path (could come from context in future)
project_path = Path.home() / ".basic-memory" / "projects" / "default"
# Get services with proper lifecycle management
async with deps.get_project_services(project_path) as memory_service:
match name:
case "create_entities":
params = CreateEntitiesParams(**arguments)
result = await memory_service.create_entities(params.entities)
return [TextContent(type="text", text=str(result))]
case "search_nodes":
params = SearchNodesParams(**arguments)
result = await memory_service.search_nodes(params.query)
return [TextContent(type="text", text=str(result))]
case _:
raise McpError(
METHOD_NOT_FOUND,
f"Unknown tool: {name}"
)
except ValueError as e:
raise McpError(INVALID_PARAMS, str(e))
except Exception as e:
raise McpError(INTERNAL_ERROR, str(e))
server = MemoryServer()
async def run_server():
"""Run the MCP server."""
+21 -13
View File
@@ -7,8 +7,7 @@ from datetime import datetime, UTC
from typing import List, Optional, Dict, Any
from uuid import uuid4
from pydantic import BaseModel, model_validator
from pydantic import BaseModel, Field, model_validator
class ObservationIn(BaseModel):
"""Schema for creating a single observation."""
@@ -17,29 +16,37 @@ class ObservationIn(BaseModel):
class ObservationsIn(BaseModel):
"""Schema for adding observations to an entity."""
entity_id: str # Maps to Entity.id
entity_id: str = Field(alias="entityId") # Maps to Entity.id
observations: List[ObservationIn]
class Config:
populate_by_name = True
class ObservationOut(ObservationIn):
"""Schema for observation data returned from the service."""
id: int
class ObservationsOut(BaseModel):
"""Schema for bulk observation operation results."""
entity_id: str
entity_id: str = Field(alias="entityId")
observations: List[ObservationOut]
class Config:
populate_by_name = True
class RelationIn(BaseModel):
"""
Represents a directed edge between entities in the knowledge graph.
Relations are always stored in active voice (e.g. "created", "teaches", etc.)
"""
from_id: str
to_id: str
relation_type: str
from_id: str = Field(alias="fromId")
to_id: str = Field(alias="toId")
relation_type: str = Field(alias="relationType")
context: Optional[str] = None
class Config:
populate_by_name = True
class RelationOut(BaseModel):
id: int
@@ -47,7 +54,7 @@ class EntityBase(BaseModel):
# id assigned at creation via model_validator
id: str
name: str
entity_type: str
entity_type: str = Field(alias="entityType")
@model_validator(mode='before')
@classmethod
@@ -72,12 +79,13 @@ class EntityIn(EntityBase):
observations: List[ObservationIn] = []
relations: List[RelationIn] = []
class Config:
populate_by_name = True
class EntityOut(EntityBase):
"""
Represents a node in our knowledge graph - could be a person, project,
concept, etc. Each entity has a unique name, a type, and a list of
associated observations.
"""
"""Schema for entity data returned from the service."""
observations: List[ObservationOut] = []
relations: List[RelationOut] = []
class Config:
populate_by_name = True