diff options
| author | mo khan <mo@mokhan.ca> | 2025-06-23 15:07:58 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-06-23 15:07:58 -0600 |
| commit | d3cadfbf00214febbdfcb9f1f8ebd5a747b55486 (patch) | |
| tree | 62d75f84d61ff1be650a8a4519faeaa79a973feb | |
| parent | 4c3e991b93e9b17b12a73fb0af66d922000cf7d7 (diff) | |
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>
| -rw-r--r-- | cmd/del/main.go | 343 |
1 files changed, 343 insertions, 0 deletions
diff --git a/cmd/del/main.go b/cmd/del/main.go index 206c8d1..54798c2 100644 --- a/cmd/del/main.go +++ b/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") |
