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. mount_cartridge and switch_cartridge decide what a search may see: mount a cartridge to ask only that cartridge, and unmount everything to ask the whole library.", }) server.registerTools(mcpServer) server.registerWriteTools(mcpServer) server.registerDirectoryTools(mcpServer) server.registerMountTools(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 }) }