Commit d3cadfb

mo khan <mo@mokhan.ca>
2025-06-23 21:07:58
feat: integrate MCP memory server for persistent conversation memory
Major features added: - MCP server communication infrastructure with JSON-RPC protocol - Three new memory tools: remember, recall, forget - Persistent memory storage across Del conversations - Non-blocking MCP memory initialization on startup - Proper cleanup of MCP connections on exit - Updated system prompt to include memory capabilities Memory Tools: - remember: Store information with optional entity categorization - recall: Search and retrieve information from memory - forget: Delete specific entities from memory Del now has persistent memory to learn and improve over time\! ๐Ÿค– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 4c3e991
Changed files (1)
cmd
cmd/del/main.go
@@ -51,6 +51,7 @@ type Del struct {
 	thinking    bool
 	thinkingMsg string
 	startTime   time.Time
+	mcpMemory   *MCPServer
 }
 
 type ToolFunc func(ctx context.Context, args map[string]interface{}, progress chan<- StreamMessage) (string, error)
@@ -59,6 +60,34 @@ type ToolCall struct {
 	Args map[string]interface{}
 }
 
+// MCP Integration types
+type MCPRequest struct {
+	JSONRPC string      `json:"jsonrpc"`
+	ID      int         `json:"id"`
+	Method  string      `json:"method"`
+	Params  interface{} `json:"params"`
+}
+
+type MCPResponse struct {
+	JSONRPC string      `json:"jsonrpc"`
+	ID      int         `json:"id"`
+	Result  interface{} `json:"result,omitempty"`
+	Error   *MCPError   `json:"error,omitempty"`
+}
+
+type MCPError struct {
+	Code    int    `json:"code"`
+	Message string `json:"message"`
+}
+
+type MCPServer struct {
+	name    string
+	command string
+	process *exec.Cmd
+	stdin   io.WriteCloser
+	stdout  io.ReadCloser
+}
+
 func NewDel(model string) *Del {
 	client, _ := api.ClientFromEnvironment()
 	
@@ -80,6 +109,7 @@ Available tools:
 โ€ข PROJECT MANAGEMENT: todo_read, todo_write, exit_plan_mode  
 โ€ข NOTEBOOKS: notebook_read, notebook_edit (Jupyter support)
 โ€ข WEB OPERATIONS: web_fetch, web_search
+โ€ข MEMORY SYSTEM: remember, recall, forget (persistent memory across conversations)
 
 KEY CAPABILITIES:
 - Edit files with exact string replacement (edit_file) or multiple edits (multi_edit)
@@ -98,6 +128,8 @@ EXAMPLES:
 - "run tests" โ†’ use bash with command "npm test"
 - "edit config.yaml" โ†’ use edit_file to make precise changes
 - "show todos" โ†’ use todo_read
+- "remember that the user prefers tabs over spaces" โ†’ use remember
+- "what did we learn about Go best practices?" โ†’ use recall
 
 FORMATTING: Always format responses using markdown:
 - ## Headers for sections
@@ -113,6 +145,17 @@ IMPORTANT: Use tools first, then provide natural markdown responses based on res
 	}
 	
 	d.registerTools()
+	
+	// Initialize MCP memory (non-blocking)
+	go func() {
+		if err := d.startMCPMemory(); err != nil {
+			d.emit(StreamMessage{
+				Type:  MessageTypeSystem,
+				Error: fmt.Sprintf("Warning: Failed to initialize memory system: %v", err),
+			})
+		}
+	}()
+	
 	return d
 }
 
@@ -348,6 +391,229 @@ func (d *Del) registerTools() {
 	// Web operations
 	d.tools["web_fetch"] = d.webFetch
 	d.tools["web_search"] = d.webSearch
+	
+	// MCP Memory operations
+	d.tools["remember"] = d.remember
+	d.tools["recall"] = d.recall
+	d.tools["forget"] = d.forget
+}
+
+// MCP Server Management
+func (d *Del) startMCPMemory() error {
+	cmd := exec.Command("/usr/local/bin/mcp-memory")
+	
+	stdin, err := cmd.StdinPipe()
+	if err != nil {
+		return fmt.Errorf("failed to create stdin pipe: %v", err)
+	}
+	
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		return fmt.Errorf("failed to create stdout pipe: %v", err)
+	}
+	
+	if err := cmd.Start(); err != nil {
+		return fmt.Errorf("failed to start mcp-memory: %v", err)
+	}
+	
+	d.mcpMemory = &MCPServer{
+		name:    "mcp-memory",
+		command: "/usr/local/bin/mcp-memory",
+		process: cmd,
+		stdin:   stdin,
+		stdout:  stdout,
+	}
+	
+	// Initialize MCP connection
+	initReq := MCPRequest{
+		JSONRPC: "2.0",
+		ID:      1,
+		Method:  "initialize",
+		Params: map[string]interface{}{
+			"protocolVersion": "2024-11-05",
+			"capabilities":    map[string]interface{}{"tools": map[string]interface{}{}},
+			"clientInfo":      map[string]interface{}{"name": "del", "version": "1.0.0"},
+		},
+	}
+	
+	_, err = d.callMCP(d.mcpMemory, initReq)
+	return err
+}
+
+func (d *Del) callMCP(server *MCPServer, req MCPRequest) (*MCPResponse, error) {
+	// Send request
+	reqJSON, err := json.Marshal(req)
+	if err != nil {
+		return nil, fmt.Errorf("failed to marshal request: %v", err)
+	}
+	
+	if _, err := server.stdin.Write(append(reqJSON, '\n')); err != nil {
+		return nil, fmt.Errorf("failed to write request: %v", err)
+	}
+	
+	// Read response
+	scanner := bufio.NewScanner(server.stdout)
+	if !scanner.Scan() {
+		return nil, fmt.Errorf("failed to read response")
+	}
+	
+	var resp MCPResponse
+	if err := json.Unmarshal(scanner.Bytes(), &resp); err != nil {
+		return nil, fmt.Errorf("failed to unmarshal response: %v", err)
+	}
+	
+	if resp.Error != nil {
+		return nil, fmt.Errorf("MCP error %d: %s", resp.Error.Code, resp.Error.Message)
+	}
+	
+	return &resp, nil
+}
+
+// Memory Tools
+func (d *Del) remember(ctx context.Context, args map[string]interface{}, progress chan<- StreamMessage) (string, error) {
+	content, ok := args["content"].(string)
+	if !ok {
+		return "", fmt.Errorf("missing 'content' argument")
+	}
+	
+	entity, _ := args["entity"].(string)
+	if entity == "" {
+		entity = "general_knowledge"
+	}
+	
+	progress <- StreamMessage{
+		Type:     MessageTypeProgress,
+		ToolName: "remember",
+		Status:   "storing",
+		Content:  "Storing in memory...",
+	}
+	
+	if d.mcpMemory == nil {
+		return "", fmt.Errorf("MCP memory not initialized")
+	}
+	
+	// Create entity for this memory
+	createReq := MCPRequest{
+		JSONRPC: "2.0",
+		ID:      2,
+		Method:  "tools/call",
+		Params: map[string]interface{}{
+			"name": "create_entities",
+			"arguments": map[string]interface{}{
+				"entities": []map[string]interface{}{
+					{
+						"name":        entity,
+						"entityType":  "concept",
+						"observations": []string{content},
+					},
+				},
+			},
+		},
+	}
+	
+	resp, err := d.callMCP(d.mcpMemory, createReq)
+	if err != nil {
+		return "", fmt.Errorf("failed to create memory: %v", err)
+	}
+	
+	progress <- StreamMessage{
+		Type:     MessageTypeProgress,
+		ToolName: "remember",
+		Status:   "completed",
+		Content:  "Memory stored successfully",
+	}
+	
+	return fmt.Sprintf("Remembered: %s\nStored as entity: %s\nResponse: %v", content, entity, resp.Result), nil
+}
+
+func (d *Del) recall(ctx context.Context, args map[string]interface{}, progress chan<- StreamMessage) (string, error) {
+	query, ok := args["query"].(string)
+	if !ok {
+		return "", fmt.Errorf("missing 'query' argument")
+	}
+	
+	progress <- StreamMessage{
+		Type:     MessageTypeProgress,
+		ToolName: "recall",
+		Status:   "searching",
+		Content:  fmt.Sprintf("Searching memory for: %s", query),
+	}
+	
+	if d.mcpMemory == nil {
+		return "", fmt.Errorf("MCP memory not initialized")
+	}
+	
+	// Search memory
+	searchReq := MCPRequest{
+		JSONRPC: "2.0",
+		ID:      3,
+		Method:  "tools/call",
+		Params: map[string]interface{}{
+			"name": "search_nodes",
+			"arguments": map[string]interface{}{
+				"query": query,
+			},
+		},
+	}
+	
+	resp, err := d.callMCP(d.mcpMemory, searchReq)
+	if err != nil {
+		return "", fmt.Errorf("failed to search memory: %v", err)
+	}
+	
+	progress <- StreamMessage{
+		Type:     MessageTypeProgress,
+		ToolName: "recall",
+		Status:   "completed",
+		Content:  "Memory search completed",
+	}
+	
+	return fmt.Sprintf("Memory search for '%s':\n%v", query, resp.Result), nil
+}
+
+func (d *Del) forget(ctx context.Context, args map[string]interface{}, progress chan<- StreamMessage) (string, error) {
+	entity, ok := args["entity"].(string)
+	if !ok {
+		return "", fmt.Errorf("missing 'entity' argument")
+	}
+	
+	progress <- StreamMessage{
+		Type:     MessageTypeProgress,
+		ToolName: "forget",
+		Status:   "deleting",
+		Content:  fmt.Sprintf("Forgetting: %s", entity),
+	}
+	
+	if d.mcpMemory == nil {
+		return "", fmt.Errorf("MCP memory not initialized")
+	}
+	
+	// Delete entity
+	deleteReq := MCPRequest{
+		JSONRPC: "2.0",
+		ID:      4,
+		Method:  "tools/call",
+		Params: map[string]interface{}{
+			"name": "delete_entities",
+			"arguments": map[string]interface{}{
+				"entityIds": []string{entity},
+			},
+		},
+	}
+	
+	resp, err := d.callMCP(d.mcpMemory, deleteReq)
+	if err != nil {
+		return "", fmt.Errorf("failed to delete memory: %v", err)
+	}
+	
+	progress <- StreamMessage{
+		Type:     MessageTypeProgress,
+		ToolName: "forget",
+		Status:   "completed",
+		Content:  "Memory deleted successfully",
+	}
+	
+	return fmt.Sprintf("Forgot: %s\nResponse: %v", entity, resp.Result), nil
 }
 
 func (d *Del) readFile(ctx context.Context, args map[string]interface{}, progress chan<- StreamMessage) (string, error) {
@@ -2011,6 +2277,67 @@ func (d *Del) buildOllamaTools() []api.Tool {
 		Function: webSearchFunc,
 	})
 	
+	// remember tool
+	rememberFunc := api.ToolFunction{
+		Name:        "remember",
+		Description: "Store information in persistent memory for future recall",
+	}
+	rememberFunc.Parameters.Type = "object"
+	rememberFunc.Parameters.Required = []string{"content"}
+	rememberFunc.Parameters.Properties = make(map[string]struct {
+		Type        api.PropertyType `json:"type"`
+		Items       any              `json:"items,omitempty"`
+		Description string           `json:"description"`
+		Enum        []any            `json:"enum,omitempty"`
+	})
+	rememberFunc.Parameters.Properties["content"] = makeProperty("string", "Information to remember")
+	rememberFunc.Parameters.Properties["entity"] = makeProperty("string", "Optional entity/category for organization")
+	
+	tools = append(tools, api.Tool{
+		Type:     "function",
+		Function: rememberFunc,
+	})
+	
+	// recall tool
+	recallFunc := api.ToolFunction{
+		Name:        "recall",
+		Description: "Search and retrieve information from persistent memory",
+	}
+	recallFunc.Parameters.Type = "object"
+	recallFunc.Parameters.Required = []string{"query"}
+	recallFunc.Parameters.Properties = make(map[string]struct {
+		Type        api.PropertyType `json:"type"`
+		Items       any              `json:"items,omitempty"`
+		Description string           `json:"description"`
+		Enum        []any            `json:"enum,omitempty"`
+	})
+	recallFunc.Parameters.Properties["query"] = makeProperty("string", "Search query for memory recall")
+	
+	tools = append(tools, api.Tool{
+		Type:     "function",
+		Function: recallFunc,
+	})
+	
+	// forget tool
+	forgetFunc := api.ToolFunction{
+		Name:        "forget",
+		Description: "Delete specific information from persistent memory",
+	}
+	forgetFunc.Parameters.Type = "object"
+	forgetFunc.Parameters.Required = []string{"entity"}
+	forgetFunc.Parameters.Properties = make(map[string]struct {
+		Type        api.PropertyType `json:"type"`
+		Items       any              `json:"items,omitempty"`
+		Description string           `json:"description"`
+		Enum        []any            `json:"enum,omitempty"`
+	})
+	forgetFunc.Parameters.Properties["entity"] = makeProperty("string", "Entity/memory to delete")
+	
+	tools = append(tools, api.Tool{
+		Type:     "function",
+		Function: forgetFunc,
+	})
+	
 	return tools
 }
 
@@ -2378,6 +2705,7 @@ func (d *Del) Start(ctx context.Context) {
 		
 		if input == "quit" || input == "exit" || input == "q" {
 			fmt.Println("๐Ÿ‘‹ Stay funky!")
+			d.cleanup()
 			break
 		}
 		
@@ -2390,6 +2718,21 @@ func (d *Del) Start(ctx context.Context) {
 	close(d.output)
 }
 
+func (d *Del) cleanup() {
+	if d.mcpMemory != nil {
+		if d.mcpMemory.stdin != nil {
+			d.mcpMemory.stdin.Close()
+		}
+		if d.mcpMemory.stdout != nil {
+			d.mcpMemory.stdout.Close()
+		}
+		if d.mcpMemory.process != nil {
+			d.mcpMemory.process.Process.Kill()
+			d.mcpMemory.process.Wait()
+		}
+	}
+}
+
 func main() {
 	var model = flag.String("model", "qwen2.5:latest", "Ollama model to use")
 	var help = flag.Bool("help", false, "Show help message")