Local read-only Model Context Protocol server (Go) exposing the .ely_eye memory store to coding agents such as Codex and Claude Code: nine tools over status, cartridges, Evidence Atoms, compiled evidence packs, proof suites, and HashHop proofs. One-line cross-platform install (no Docker), with a prebuilt-binary release workflow.
137 lines
3.8 KiB
Go
137 lines
3.8 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"net/url"
|
|
"path/filepath"
|
|
|
|
"github.com/ZacharyZhang-NY/ely-eye/ely-eye-mcp/internal/config"
|
|
"github.com/ZacharyZhang-NY/ely-eye/ely-eye-mcp/internal/domain"
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
type Store struct {
|
|
cfg config.Config
|
|
db *sql.DB
|
|
}
|
|
|
|
func Open(ctx context.Context, cfg config.Config) (*Store, error) {
|
|
dsn := sqliteReadOnlyDSN(cfg.Database)
|
|
db, err := sql.Open("sqlite", dsn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
db.SetMaxOpenConns(1)
|
|
if err := db.PingContext(ctx); err != nil {
|
|
db.Close()
|
|
return nil, err
|
|
}
|
|
return &Store{cfg: cfg, db: db}, nil
|
|
}
|
|
|
|
func (s *Store) Close() error {
|
|
return s.db.Close()
|
|
}
|
|
|
|
func (s *Store) Status(ctx context.Context) (domain.Status, error) {
|
|
atomCount, err := s.scalarInt(ctx, "SELECT COUNT(*) FROM atoms")
|
|
if err != nil {
|
|
return domain.Status{}, err
|
|
}
|
|
sourceCount, err := s.scalarInt(ctx, "SELECT COUNT(*) FROM sources")
|
|
if err != nil {
|
|
return domain.Status{}, err
|
|
}
|
|
cartridgeCount, err := s.scalarInt(ctx, "SELECT COUNT(*) FROM cartridges")
|
|
if err != nil {
|
|
return domain.Status{}, err
|
|
}
|
|
activeTokens, err := s.scalarInt64(ctx, "SELECT COALESCE(SUM(token_equivalent), 0) FROM atoms")
|
|
if err != nil {
|
|
return domain.Status{}, err
|
|
}
|
|
cartridgeTokens, err := s.scalarInt64(ctx, "SELECT COALESCE(SUM(token_equivalent), 0) FROM cartridges")
|
|
if err != nil {
|
|
return domain.Status{}, err
|
|
}
|
|
layers, err := s.CacheLayers(ctx)
|
|
if err != nil {
|
|
return domain.Status{}, err
|
|
}
|
|
libraryTokens := activeTokens
|
|
if cartridgeTokens > libraryTokens {
|
|
libraryTokens = cartridgeTokens
|
|
}
|
|
return domain.Status{
|
|
App: "Ely-Eye MCP",
|
|
ProjectRoot: s.cfg.ProjectRoot,
|
|
DataHome: s.cfg.ElyEyeHome,
|
|
Database: s.cfg.Database,
|
|
AtomCount: atomCount,
|
|
SourceCount: sourceCount,
|
|
CartridgeCount: cartridgeCount,
|
|
ActiveTokenEquivalent: activeTokens,
|
|
LibraryTokenEquivalent: libraryTokens,
|
|
CacheLayers: layers,
|
|
}, nil
|
|
}
|
|
|
|
func (s *Store) CacheLayers(ctx context.Context) ([]domain.CacheLayer, error) {
|
|
rows, err := s.db.QueryContext(ctx, `
|
|
SELECT
|
|
layer,
|
|
COUNT(DISTINCT cache_key) AS entries,
|
|
COALESCE(SUM(bytes_estimate), 0) AS bytes_estimate,
|
|
SUM(CASE WHEN event = 'hit' THEN 1 ELSE 0 END) AS hit_count,
|
|
SUM(CASE WHEN event = 'miss' THEN 1 ELSE 0 END) AS miss_count,
|
|
COALESCE(MAX(ts), '') AS last_event_at
|
|
FROM cache_events
|
|
GROUP BY layer
|
|
ORDER BY layer`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
layers := []domain.CacheLayer{}
|
|
for rows.Next() {
|
|
var layer domain.CacheLayer
|
|
if err := rows.Scan(&layer.Layer, &layer.Entries, &layer.BytesEstimate, &layer.HitCount, &layer.MissCount, &layer.LastEventAt); err != nil {
|
|
return nil, err
|
|
}
|
|
layers = append(layers, layer)
|
|
}
|
|
return layers, rows.Err()
|
|
}
|
|
|
|
func (s *Store) scalarInt(ctx context.Context, query string) (int, error) {
|
|
var value int
|
|
if err := s.db.QueryRowContext(ctx, query).Scan(&value); err != nil {
|
|
return 0, err
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func (s *Store) scalarInt64(ctx context.Context, query string) (int64, error) {
|
|
var value int64
|
|
if err := s.db.QueryRowContext(ctx, query).Scan(&value); err != nil {
|
|
return 0, err
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func sqliteReadOnlyDSN(path string) string {
|
|
absolute, _ := filepath.Abs(path)
|
|
uri := "file:" + filepath.ToSlash(absolute)
|
|
query := url.Values{}
|
|
query.Set("mode", "ro")
|
|
query.Add("_pragma", "query_only(1)")
|
|
// The production database runs in WAL mode and is written by the Ely-Eye
|
|
// app concurrently. busy_timeout lets a reader wait through a checkpoint or
|
|
// a momentary write lock instead of failing with SQLITE_BUSY. immutable is
|
|
// deliberately not set because the database does change underneath us.
|
|
query.Add("_pragma", "busy_timeout(5000)")
|
|
return fmt.Sprintf("%s?%s", uri, query.Encode())
|
|
}
|