remember / supersede / forget now reach the one Python writer from a terminal, from MCP, and from HTTP. The server, not the caller, decides who owns a memory. - cli: remember, supersede, forget. --holder falls back to settings.default_holder until the holder registry lands. Timestamps are parsed once with one clear message. - ingestion: import pymupdf as fitz. The deprecated fitz import printed a warning to stdout, so every CLI command's JSON answer was unparseable. - mcp: remember, supersede_memory and forget_memory forward to the Python writer over HTTP (ELY_EYE_API, default http://127.0.0.1:8765). SQLite stays read-only to the Go side, so the write rules live in one place. - mcp reads follow schema v2: atoms.time became ingested_at, and reads go through v_atoms_current, so a superseded memory stops being findable. holder, origin, scenario and event_time now show up in fetch_atom and search_atoms. - scripts/mcp_write_smoke.sh proves the loop end to end: remember, search, supersede, forget, then not found. Evidence: 92 pytest passed; go build, go vet and go test clean; mypy clean on the 10 touched source files with a fresh cache; ruff clean except a pre-existing B008 at cli.py:292, identical at HEAD.
94 lines
4.0 KiB
Go
94 lines
4.0 KiB
Go
package mcpserver
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"github.com/ZacharyZhang-NY/ely-eye/ely-eye-mcp/internal/domain"
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
type RememberInput struct {
|
|
Text string `json:"text" jsonschema:"the statement to remember, kept verbatim"`
|
|
Scenario string `json:"scenario,omitempty" jsonschema:"optional context label such as home or work"`
|
|
EventTime string `json:"event_time,omitempty" jsonschema:"when it was true, ISO 8601 with offset, e.g. 2026-05-01T12:00:00+08:00"`
|
|
EventTimePrecision string `json:"event_time_precision,omitempty" jsonschema:"instant, minute, hour, day, month, or year; required with event_time"`
|
|
RetentionUntil string `json:"retention_until,omitempty" jsonschema:"ISO 8601 with offset; the memory stops being retrievable after this"`
|
|
}
|
|
|
|
type SupersedeInput struct {
|
|
AtomID string `json:"atom_id" jsonschema:"exact Evidence Atom id being replaced"`
|
|
Text string `json:"text" jsonschema:"the newer statement, kept verbatim"`
|
|
Scenario string `json:"scenario,omitempty" jsonschema:"optional context label such as home or work"`
|
|
EventTime string `json:"event_time,omitempty" jsonschema:"when the new statement became true, ISO 8601 with offset"`
|
|
EventTimePrecision string `json:"event_time_precision,omitempty" jsonschema:"instant, minute, hour, day, month, or year; required with event_time"`
|
|
}
|
|
|
|
type ForgetInput struct {
|
|
AtomID string `json:"atom_id,omitempty" jsonschema:"exact Evidence Atom id to forget"`
|
|
SourceID string `json:"source_id,omitempty" jsonschema:"exact source id to forget, removing every atom of that source"`
|
|
Reason string `json:"reason,omitempty" jsonschema:"why this memory is gone"`
|
|
}
|
|
|
|
type RememberOutput struct {
|
|
Atom domain.Memory `json:"atom"`
|
|
}
|
|
|
|
type ForgetOutput struct {
|
|
Report domain.ForgetReport `json:"report"`
|
|
}
|
|
|
|
func (s *Server) registerWriteTools(server *mcp.Server) {
|
|
mcp.AddTool(server, &mcp.Tool{
|
|
Name: "remember",
|
|
Title: "Remember",
|
|
Description: "Store a new memory. The Ely-Eye server stamps the holder, origin and ingestion time, so a caller cannot claim to be somebody else.",
|
|
}, s.rememberTool)
|
|
mcp.AddTool(server, &mcp.Tool{
|
|
Name: "supersede_memory",
|
|
Title: "Supersede a memory",
|
|
Description: "Replace a memory with a newer statement. The old one stops being retrievable but keeps its record, so the timeline stays readable.",
|
|
}, s.supersedeTool)
|
|
mcp.AddTool(server, &mcp.Tool{
|
|
Name: "forget_memory",
|
|
Title: "Forget a memory",
|
|
Description: "Delete a memory by atom_id or source_id and drop the derived indexes that would keep surfacing it.",
|
|
}, s.forgetTool)
|
|
}
|
|
|
|
func (s *Server) rememberTool(ctx context.Context, _ *mcp.CallToolRequest, input RememberInput) (*mcp.CallToolResult, RememberOutput, error) {
|
|
if input.Text == "" {
|
|
return nil, RememberOutput{}, errors.New("text is required")
|
|
}
|
|
var atom domain.Memory
|
|
if err := s.api.Post(ctx, "/api/memory", input, &atom); err != nil {
|
|
return nil, RememberOutput{}, err
|
|
}
|
|
return nil, RememberOutput{Atom: atom}, nil
|
|
}
|
|
|
|
func (s *Server) supersedeTool(ctx context.Context, _ *mcp.CallToolRequest, input SupersedeInput) (*mcp.CallToolResult, RememberOutput, error) {
|
|
if input.AtomID == "" {
|
|
return nil, RememberOutput{}, errors.New("atom_id is required")
|
|
}
|
|
if input.Text == "" {
|
|
return nil, RememberOutput{}, errors.New("text is required")
|
|
}
|
|
var atom domain.Memory
|
|
if err := s.api.Post(ctx, "/api/memory/supersede", input, &atom); err != nil {
|
|
return nil, RememberOutput{}, err
|
|
}
|
|
return nil, RememberOutput{Atom: atom}, nil
|
|
}
|
|
|
|
func (s *Server) forgetTool(ctx context.Context, _ *mcp.CallToolRequest, input ForgetInput) (*mcp.CallToolResult, ForgetOutput, error) {
|
|
if (input.AtomID == "") == (input.SourceID == "") {
|
|
return nil, ForgetOutput{}, errors.New("forget takes exactly one of atom_id or source_id")
|
|
}
|
|
var report domain.ForgetReport
|
|
if err := s.api.Post(ctx, "/api/memory/forget", input, &report); err != nil {
|
|
return nil, ForgetOutput{}, err
|
|
}
|
|
return nil, ForgetOutput{Report: report}, nil
|
|
}
|