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.
87 lines
2.9 KiB
Go
87 lines
2.9 KiB
Go
package mcpserver
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/ZacharyZhang-NY/ely-eye/ely-eye-mcp/internal/apiclient"
|
|
"github.com/ZacharyZhang-NY/ely-eye/ely-eye-mcp/internal/config"
|
|
"github.com/ZacharyZhang-NY/ely-eye/ely-eye-mcp/internal/store"
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
type Server struct {
|
|
cfg config.Config
|
|
store *store.Store
|
|
api *apiclient.Client
|
|
version string
|
|
}
|
|
|
|
func Run(ctx context.Context, cfg config.Config, version string) error {
|
|
db, err := store.Open(ctx, cfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer db.Close()
|
|
server := &Server{cfg: cfg, store: db, api: apiclient.New(cfg.APIBaseURL), version: version}
|
|
mcpServer := mcp.NewServer(&mcp.Implementation{
|
|
Name: "ely-eye",
|
|
Title: "Ely-Eye Context Cartridge MCP",
|
|
Version: version,
|
|
}, &mcp.ServerOptions{
|
|
Instructions: "Use Ely-Eye tools to retrieve local cartridge context, evidence atoms, proof suites, and compiled evidence packs. Read tools mirror local Ely-Eye state without writing. remember, supersede_memory and forget_memory mutate memory through the Ely-Eye API, which must be running (`ely-eye serve`); the server, not the caller, decides who owns a memory.",
|
|
})
|
|
server.registerTools(mcpServer)
|
|
server.registerWriteTools(mcpServer)
|
|
server.registerResources(mcpServer)
|
|
if err := mcpServer.Run(ctx, &mcp.StdioTransport{}); err != nil {
|
|
if errors.Is(err, io.EOF) || strings.HasSuffix(err.Error(), "EOF") {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) registerResources(server *mcp.Server) {
|
|
addFileResource(server, "ely-eye://project/prd", "Ely-Eye PRD", filepath.Join(s.cfg.ProjectRoot, "PRD.md"))
|
|
addFileResource(server, "ely-eye://project/readme", "Ely-Eye README", filepath.Join(s.cfg.ProjectRoot, "README.md"))
|
|
addFileResource(server, "ely-eye://project/mcp-research", "Ely-Eye MCP research notes", filepath.Join(s.cfg.ProjectRoot, "ely-eye-mcp", "docs", "RESEARCH.md"))
|
|
}
|
|
|
|
// addFileResource registers a project document only when it exists on disk.
|
|
// External deployments rarely ship the Ely-Eye repository documents, so a
|
|
// missing file is a normal condition and the resource is simply omitted.
|
|
func addFileResource(server *mcp.Server, uri string, name string, path string) {
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return
|
|
}
|
|
size := info.Size()
|
|
server.AddResource(&mcp.Resource{
|
|
URI: uri,
|
|
Name: name,
|
|
Title: name,
|
|
Description: "Local Ely-Eye project resource: " + path,
|
|
MIMEType: "text/markdown",
|
|
Size: size,
|
|
}, func(ctx context.Context, request *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
|
|
_ = ctx
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &mcp.ReadResourceResult{
|
|
Contents: []*mcp.ResourceContents{{
|
|
URI: request.Params.URI,
|
|
MIMEType: "text/markdown",
|
|
Text: string(data),
|
|
}},
|
|
}, nil
|
|
})
|
|
}
|