Add Ely-Eye MCP server

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.
This commit is contained in:
ZacharyZhang-NY committed 2026-05-30 23:51:45 -04:00
1 parent 1392f4456d
commit 4cd29f91e5
23 files changed
+2463 -3

No files matched your search

+76
View File
@@ -0,0 +1,76 @@
name: ely-eye-mcp release
on:
push:
tags:
- "mcp-v*"
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- { goos: linux, goarch: amd64 }
- { goos: linux, goarch: arm64 }
- { goos: darwin, goarch: amd64 }
- { goos: darwin, goarch: arm64 }
- { goos: windows, goarch: amd64 }
- { goos: windows, goarch: arm64 }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.25"
cache-dependency-path: ely-eye-mcp/go.sum
- name: Build and archive
working-directory: ely-eye-mcp
env:
CGO_ENABLED: "0"
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
run: |
set -euo pipefail
version="${GITHUB_REF_NAME#mcp-}"
binary="ely-eye-mcp"
if [ "$GOOS" = "windows" ]; then binary="ely-eye-mcp.exe"; fi
ldflags="-s -w -X github.com/ZacharyZhang-NY/ely-eye/ely-eye-mcp/internal/cli.version=${version}"
mkdir -p dist
go build -trimpath -ldflags "$ldflags" -o "dist/${binary}" ./cmd/ely-eye-mcp
base="ely-eye-mcp_${GOOS}_${GOARCH}"
cd dist
if [ "$GOOS" = "windows" ]; then
zip "${base}.zip" "$binary"
else
tar -czf "${base}.tar.gz" "$binary"
fi
rm -f "$binary"
- uses: actions/upload-artifact@v4
with:
name: dist-${{ matrix.goos }}-${{ matrix.goarch }}
path: ely-eye-mcp/dist/*
if-no-files-found: error
release:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: Generate checksums
run: |
set -euo pipefail
cd dist
# Match only the per-target archives (ely-eye-mcp_<os>_<arch>.*); the
# single-underscore SHA256SUMS file is excluded so it never hashes itself.
sha256sum ely-eye-mcp_*_* > ely-eye-mcp_SHA256SUMS.txt
- uses: softprops/action-gh-release@v2
with:
files: dist/*
fail_on_unmatched_files: true
generate_release_notes: true
+30 -3
View File
@@ -99,6 +99,32 @@ evidence under `.ely_eye/data/eval_proofs/`. Among them:
.\.venv\Scripts\ely-eye.exe visual-contradiction-proof
```
## MCP server for coding agents
`ely-eye-mcp/` is a local Model Context Protocol server that exposes the
`.ely_eye` memory store to coding agents such as Codex and Claude Code. One
command builds a native Go binary (or downloads a prebuilt one), installs it
under `.ely_eye/bin/`, and registers it with both clients. No Docker.
Windows:
```powershell
powershell -ExecutionPolicy Bypass -File .\ely-eye-mcp\install.ps1 -Client both
```
Linux and macOS:
```bash
bash ./ely-eye-mcp/install.sh --client both
```
The agent then has nine read-only tools over the live memory — status,
cartridge listing and detail, Evidence Atom search and fetch, evidence-pack
compilation, proof suites, and HashHop proofs. The server opens the database
read-only and never writes to it. The same installer works against any
`.ely_eye` store, not only this repository. See `ely-eye-mcp/README.md` for the
full tool list and the release pipeline.
## Data layout
Runtime state lives under `.ely_eye/` (git-ignored):
@@ -120,7 +146,8 @@ capsule, any attached adapters, and the eval proofs.
## Project layout
```text
backend/ Python: ingestion, retrieval, runtime, training, proof suite, FastAPI
frontend/ the local dashboard (Vite + React)
scripts/ PowerShell: install, model download, serving, runtimes, fine-tuning
backend/ Python: ingestion, retrieval, runtime, training, proof suite, FastAPI
frontend/ the local dashboard (Vite + React)
scripts/ PowerShell: install, model download, serving, runtimes, fine-tuning
ely-eye-mcp/ Go: local MCP server exposing the memory store to coding agents
```
+18
View File
@@ -0,0 +1,18 @@
# Deterministic line endings for the MCP project on Windows, Linux, and macOS.
# A bash installer checked out with CRLF has a broken shebang on Unix, so shell
# scripts are pinned to LF regardless of the user's core.autocrlf setting.
* text=auto
*.sh text eol=lf
*.ps1 text eol=crlf
*.go text eol=lf
*.mod text eol=lf
*.sum text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.json text eol=lf
*.md text eol=lf
*.exe binary
*.tar.gz binary
*.zip binary
+77
View File
@@ -0,0 +1,77 @@
# Ely-Eye MCP
Ely-Eye MCP exposes a local Context Cartridge memory store as a stdio Model Context Protocol server for coding agents such as Codex and Claude Code.
The server is local-first and read-only against `<ely-eye-home>/data/ely_eye.sqlite`. It reads Evidence Atoms, cartridge manifests, Memory DNA, proof-suite artifacts, HashHop proofs, and compiled evidence packs. The production database is never modified.
It can be deployed for any `.ely_eye` store. Identity is anchored on the database file, not on the Ely-Eye repository layout, so the server walks up from the working directory to find `.ely_eye/data/ely_eye.sqlite`, or takes an explicit `--ely-eye-home`.
## One-command setup
The installer downloads a checksum-verified prebuilt binary for the platform when a tagged release is available, otherwise it builds from source with Go from a repository checkout. Either way it installs the binary into `<ely-eye-home>/bin` and registers the server with Codex and Claude Code. No Docker.
Windows:
```powershell
powershell -ExecutionPolicy Bypass -File .\ely-eye-mcp\install.ps1 -Client both
```
Linux and macOS:
```bash
bash ./ely-eye-mcp/install.sh --client both
```
Force a source build (requires Go 1.25 or newer):
```bash
bash ./ely-eye-mcp/install.sh --method source
```
Codex registration uses `codex mcp add`. Claude Code project registration writes `.mcp.json` at the project root; `--scope user` registers through the Claude CLI instead.
## Manual run
From inside a project that owns an `.ely_eye` store:
```bash
go run ./cmd/ely-eye-mcp server
```
Or point at an explicit store:
```bash
go run ./cmd/ely-eye-mcp server --ely-eye-home /path/to/.ely_eye
```
## Tools
`ely_eye_status` returns atom, source, cartridge, token, and cache-layer counts.
`list_cartridges` lists local Context Cartridges with manifest JSON and Memory DNA.
`get_cartridge` reads one cartridge plus artifact metadata, memory capsule index, and asset report.
`search_atoms` queries the local SQLite FTS index and returns ranked Evidence Atoms.
`fetch_atom` reads one Evidence Atom by exact id.
`compile_context` compiles a question into a local evidence pack with the same profile names used by the app (`live_demo`, `extreme_context`, `library_100m`, `research_theater`).
`list_proof_suites` lists PRD proof suites under `<ely-eye-home>/data/eval_proofs`.
`list_hashhop_proofs` aggregates HashHop and Visual HashHop long-context addressing proofs by kind, hop count, and token budget, with pass counts and example proofs. Proofs are deduplicated by content-derived id.
`get_proof_suite` reads one PRD proof suite by id, including all checks and a summary of its HashHop proofs.
## Resources
`ely-eye://project/prd`, `ely-eye://project/readme`, and `ely-eye://project/mcp-research` expose `PRD.md`, the project README, and these research notes when those files are present next to the store.
## Releases
Pushing a `mcp-v*` tag runs `.github/workflows/mcp-release.yml`, which cross-compiles the pure-Go server (CGO disabled) for Linux, macOS, and Windows on amd64 and arm64, then publishes the archives with a `SHA256SUMS` manifest to a GitHub Release. The installers consume those assets.
## Security posture
The server opens SQLite read-only with `mode=ro` and `PRAGMA query_only(1)`, and uses `busy_timeout` so reads tolerate concurrent writes by the Ely-Eye app. Tools expose structured outputs and return tool-level errors for invalid inputs so agents self-correct through normal tool calls. Proof ids are validated as single path elements before any filesystem read. The server uses stdio transport for local clients and takes credentials only from the local environment.
+16
View File
@@ -0,0 +1,16 @@
package main
import (
"context"
"fmt"
"os"
"github.com/ZacharyZhang-NY/ely-eye/ely-eye-mcp/internal/cli"
)
func main() {
if err := cli.Run(context.Background(), os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
+55
View File
@@ -0,0 +1,55 @@
# Ely-Eye MCP Research Notes
Date: 2026-05-30
## Current protocol baseline
The MCP specification latest version observed during implementation is `2025-11-25`. It defines a JSON-RPC 2.0 lifecycle with initialization, capability negotiation, normal operation, and transport-level shutdown. Server features are resources, prompts, and tools; client features include roots, sampling, and elicitation.
Local Ely-Eye deployment uses stdio transport because Codex and Claude Code both support local stdio MCP servers. HTTP remains the better fit for remote shared services with OAuth; Ely-Eye memory is local project state, so stdio keeps the trust boundary on the user's machine.
The official Go SDK is Tier 1 in the MCP SDK list and supports the latest 2025-11-25 spec in v1.4.0 and newer. This project uses `github.com/modelcontextprotocol/go-sdk` v1.6.1.
## Product mapping
Ely-Eye already materializes Context Cartridges, Evidence Atoms, memory capsule indexes, proof suites, and cache events. The MCP layer maps those into agent-native capabilities:
| Ely-Eye object | MCP surface |
|---|---|
| Evidence Atom | `search_atoms`, `fetch_atom` |
| Context Cartridge | `list_cartridges`, `get_cartridge` |
| Context Compiler | `compile_context` |
| PRD and docs | MCP resources |
| Proof Suite | `list_proof_suites`, `get_proof_suite` |
| HashHop / Visual HashHop | `list_hashhop_proofs`, `get_proof_suite` |
| Cache Fabric | `ely_eye_status` |
## Long-context research anchors
Magic's LTM-2-mini announcement establishes the design target: 100M-token context and HashHop as a stricter long-context addressing test than needle-in-a-haystack. Ely-Eye keeps 100M as a logical memory capsule and exposes the capsule through MCP as cartridge scope. Single-request full-attention execution belongs to the physical-model profile.
Qwen3.5-9B supplies the base local VLM target: 262K native context and a 1.01M YaRN profile. Qwen3-VL-Embedding-8B supplies multimodal retrieval coverage across text, images, screenshots, video, and multimodal combinations.
Native Sparse Attention, VL-Cache, SparseVLM, DuoAttention, and the TTT-E2E/Titans line shape the memory system: sparse long-context compute, modality-aware KV compression, visual token sparsification, retrieval/streaming head separation, and test-time memory formation. The MCP server exposes these as evidence and proof artifacts so coding agents can inspect what exists locally before proposing changes.
## Security anchors
Recent MCP security papers and threat taxonomies identify tool poisoning, prompt injection, over-broad tool permissions, and hidden metadata attacks as central risks. This implementation limits the first MCP milestone to read-only local tools, concise tool descriptions, structured output schemas, and project-scoped Claude configuration. Write operations can be added as separate audited tools with explicit names and narrow inputs.
## Sources
- Model Context Protocol specification 2025-11-25: https://modelcontextprotocol.io/specification/2025-11-25
- MCP Go SDK: https://github.com/modelcontextprotocol/go-sdk
- Claude Code MCP docs: https://code.claude.com/docs/en/mcp
- OpenAI Docs MCP and Codex config docs: https://developers.openai.com/learn/docs-mcp
- Magic 100M Token Context Windows: https://magic.dev/blog/100m-token-context-windows
- Qwen3.5-9B model card: https://huggingface.co/Qwen/Qwen3.5-9B
- Qwen3-VL-Embedding-8B model card: https://huggingface.co/Qwen/Qwen3-VL-Embedding-8B
- Native Sparse Attention: https://arxiv.org/abs/2502.11089
- End-to-End Test-Time Training for Long Context: https://arxiv.org/abs/2512.23675
- VL-Cache: https://arxiv.org/abs/2410.23317
- SparseVLM: https://arxiv.org/abs/2410.04417
- DuoAttention: https://arxiv.org/abs/2410.10819
- Titans: https://arxiv.org/abs/2501.00663
- Model Context Protocol security landscape: https://arxiv.org/abs/2503.23278
- MCP threat modeling and tool poisoning: https://arxiv.org/abs/2603.22489
+25
View File
@@ -0,0 +1,25 @@
module github.com/ZacharyZhang-NY/ely-eye/ely-eye-mcp
go 1.25.0
require (
github.com/modelcontextprotocol/go-sdk v1.6.1
modernc.org/sqlite v1.51.0
)
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/jsonschema-go v0.4.3 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/segmentio/asm v1.1.3 // indirect
github.com/segmentio/encoding v0.5.4 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/sys v0.42.0 // indirect
modernc.org/libc v1.72.3 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)
+67
View File
@@ -0,0 +1,67 @@
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0=
github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modelcontextprotocol/go-sdk v1.6.1 h1:0zOSupjKUxPKSocPT1Wtago+mUHU2/uZ4xSOY0FGReU=
github.com/modelcontextprotocol/go-sdk v1.6.1/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.51.0 h1:aH/MMSoayAIhozZ7uJbVTT9QO/VhzBf0J9tymmmuC/U=
modernc.org/sqlite v1.51.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+126
View File
@@ -0,0 +1,126 @@
#requires -version 5.1
[CmdletBinding()]
param(
[ValidateSet("codex", "claude", "both")]
[string]$Client = "both",
[ValidateSet("project", "user")]
[string]$Scope = "project",
[string]$ServerName = "ely-eye",
[ValidateSet("auto", "download", "source")]
[string]$Method = "auto",
[string]$Version = "",
[string]$ProjectRoot = "",
[string]$ElyEyeHome = "",
[string]$Repo = "ZacharyZhang-NY/ely-eye"
)
# Ely-Eye MCP installer for Windows. By default it downloads a prebuilt,
# checksum-verified binary for this platform and needs no toolchain. Use
# -Method source from a repository checkout to build with Go instead.
$ErrorActionPreference = "Stop"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
if (-not $ProjectRoot) {
if ($PSScriptRoot -and (Test-Path (Join-Path $PSScriptRoot "go.mod"))) {
$ProjectRoot = Split-Path -Parent $PSScriptRoot
}
else {
$ProjectRoot = (Get-Location).Path
}
}
$ProjectRoot = (Resolve-Path $ProjectRoot).Path
if (-not $ElyEyeHome) { $ElyEyeHome = Join-Path $ProjectRoot ".ely_eye" }
$BinDir = Join-Path $ElyEyeHome "bin"
New-Item -ItemType Directory -Force -Path $BinDir | Out-Null
$Binary = Join-Path $BinDir "ely-eye-mcp.exe"
function Get-Platform {
# PROCESSOR_ARCHITEW6432 holds the true machine architecture when PowerShell
# runs as an x64-emulated process on ARM64 Windows; fall back to the process
# architecture otherwise.
$arch = $env:PROCESSOR_ARCHITEW6432
if (-not $arch) { $arch = $env:PROCESSOR_ARCHITECTURE }
switch ($arch) {
"AMD64" { return "windows_amd64" }
"ARM64" { return "windows_arm64" }
default { throw "unsupported architecture: $arch" }
}
}
function Resolve-Version {
if ($Version) { return $Version }
try {
$releases = Invoke-RestMethod -Uri "https://api.github.com/repos/$Repo/releases" -Headers @{ "User-Agent" = "ely-eye-mcp-installer" }
}
catch { return "" }
$match = $releases | Where-Object { $_.tag_name -like "mcp-*" } | Select-Object -First 1
if ($match) { return $match.tag_name }
return ""
}
function Build-FromSource {
if (-not (Get-Command go -ErrorAction SilentlyContinue)) { throw "Go 1.25 or newer is required to build from source." }
if (-not ($PSScriptRoot -and (Test-Path (Join-Path $PSScriptRoot "go.mod")))) { throw "source builds must run from a repository checkout." }
Write-Host "Building ely-eye-mcp from source with Go."
$env:CGO_ENABLED = "0"
Push-Location $PSScriptRoot
try {
go build -trimpath -ldflags="-s -w" -o $Binary .\cmd\ely-eye-mcp
if ($LASTEXITCODE -ne 0) { throw "go build failed with exit code $LASTEXITCODE." }
}
finally { Pop-Location }
}
function Download-Binary([string]$Tag) {
$platform = Get-Platform
$asset = "ely-eye-mcp_$platform.zip"
$url = "https://github.com/$Repo/releases/download/$Tag/$asset"
$sumsUrl = "https://github.com/$Repo/releases/download/$Tag/ely-eye-mcp_SHA256SUMS.txt"
$tmp = Join-Path ([IO.Path]::GetTempPath()) ("ely-eye-mcp-" + [Guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Force -Path $tmp | Out-Null
try {
Write-Host "Downloading $asset from release $Tag."
$archive = Join-Path $tmp $asset
Invoke-WebRequest -Uri $url -OutFile $archive
$sumsFile = Join-Path $tmp "SHA256SUMS"
Invoke-WebRequest -Uri $sumsUrl -OutFile $sumsFile
$line = Select-String -Path $sumsFile -Pattern ("\*?" + [Regex]::Escape($asset) + "$") | Select-Object -First 1
if (-not $line) { throw "checksum for $asset not found in release." }
$expected = ($line.Line -split "\s+")[0]
$actual = (Get-FileHash -Algorithm SHA256 -Path $archive).Hash
if ($expected.ToLower() -ne $actual.ToLower()) { throw "checksum mismatch for $asset." }
Expand-Archive -Force -Path $archive -DestinationPath $tmp
Move-Item -Force -Path (Join-Path $tmp "ely-eye-mcp.exe") -Destination $Binary
}
finally { Remove-Item -Recurse -Force $tmp }
}
switch ($Method) {
"source" { Build-FromSource }
"download" {
$tag = Resolve-Version
if (-not $tag) { throw "no prebuilt release found for $Repo." }
Download-Binary $tag
}
"auto" {
$tag = Resolve-Version
if ($tag) { Download-Binary $tag }
else {
Write-Host "No prebuilt release available; building from source."
Build-FromSource
}
}
}
& $Binary setup `
--client $Client `
--scope $Scope `
--server-name $ServerName `
--project-root $ProjectRoot `
--ely-eye-home $ElyEyeHome `
--binary $Binary
+143
View File
@@ -0,0 +1,143 @@
#!/usr/bin/env bash
set -euo pipefail
# Ely-Eye MCP installer for Linux and macOS.
#
# curl -fsSL https://raw.githubusercontent.com/ZacharyZhang-NY/ely-eye/main/ely-eye-mcp/install.sh | bash
#
# By default it downloads a prebuilt, checksum-verified binary for this platform
# and needs no toolchain. Run with --method source from a repository checkout to
# build with Go instead. The binary is installed into <ely-eye-home>/bin and
# registered with Codex and Claude Code.
REPO="ZacharyZhang-NY/ely-eye"
CLIENT="both"
SCOPE="project"
SERVER_NAME="ely-eye"
METHOD="auto"
VERSION=""
PROJECT_ROOT=""
ELY_EYE_HOME=""
while [[ $# -gt 0 ]]; do
case "$1" in
--client) CLIENT="$2"; shift 2 ;;
--scope) SCOPE="$2"; shift 2 ;;
--server-name) SERVER_NAME="$2"; shift 2 ;;
--method) METHOD="$2"; shift 2 ;;
--version) VERSION="$2"; shift 2 ;;
--project-root) PROJECT_ROOT="$2"; shift 2 ;;
--ely-eye-home) ELY_EYE_HOME="$2"; shift 2 ;;
--repo) REPO="$2"; shift 2 ;;
*) echo "unknown argument: $1" >&2; exit 1 ;;
esac
done
case "$METHOD" in auto | download | source) ;; *) echo "method must be auto, download, or source" >&2; exit 1 ;; esac
SOURCE="${BASH_SOURCE[0]:-}"
SCRIPT_DIR=""
if [[ -n "$SOURCE" && -f "$SOURCE" ]]; then
SCRIPT_DIR="$(cd -- "$(dirname -- "$SOURCE")" && pwd)"
fi
if [[ -z "$PROJECT_ROOT" ]]; then
if [[ -n "$SCRIPT_DIR" && -f "$SCRIPT_DIR/go.mod" ]]; then
PROJECT_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)"
else
PROJECT_ROOT="$PWD"
fi
fi
PROJECT_ROOT="$(cd -- "$PROJECT_ROOT" && pwd)"
[[ -z "$ELY_EYE_HOME" ]] && ELY_EYE_HOME="$PROJECT_ROOT/.ely_eye"
mkdir -p "$ELY_EYE_HOME/bin"
BINARY="$ELY_EYE_HOME/bin/ely-eye-mcp"
detect_platform() {
local os arch
case "$(uname -s)" in
Linux) os="linux" ;;
Darwin) os="darwin" ;;
*) echo "unsupported operating system: $(uname -s)" >&2; exit 1 ;;
esac
case "$(uname -m)" in
x86_64 | amd64) arch="amd64" ;;
arm64 | aarch64) arch="arm64" ;;
*) echo "unsupported architecture: $(uname -m)" >&2; exit 1 ;;
esac
echo "${os}_${arch}"
}
sha256() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$1" | awk '{print $1}'
else
shasum -a 256 "$1" | awk '{print $1}'
fi
}
resolve_version() {
if [[ -n "$VERSION" ]]; then echo "$VERSION"; return 0; fi
local body
body="$(curl -fsSL "https://api.github.com/repos/${REPO}/releases" 2>/dev/null)" || return 0
printf '%s\n' "$body" \
| grep -oE '"tag_name": *"mcp-[^"]*"' \
| head -n 1 \
| sed -E 's/.*"(mcp-[^"]*)".*/\1/' || true
}
build_from_source() {
command -v go >/dev/null 2>&1 || { echo "Go 1.25 or newer is required to build from source." >&2; exit 1; }
[[ -n "$SCRIPT_DIR" && -f "$SCRIPT_DIR/go.mod" ]] || { echo "source builds must run from a repository checkout." >&2; exit 1; }
echo "Building ely-eye-mcp from source with Go."
( cd "$SCRIPT_DIR" && CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o "$BINARY" ./cmd/ely-eye-mcp )
}
download_binary() {
local tag="$1" platform asset url sums tmp
platform="$(detect_platform)"
asset="ely-eye-mcp_${platform}.tar.gz"
url="https://github.com/${REPO}/releases/download/${tag}/${asset}"
sums="https://github.com/${REPO}/releases/download/${tag}/ely-eye-mcp_SHA256SUMS.txt"
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' RETURN
echo "Downloading $asset from release $tag."
curl -fsSL "$url" -o "$tmp/$asset"
curl -fsSL "$sums" -o "$tmp/SHA256SUMS"
local expected actual
expected="$(grep -E " \*?${asset}\$" "$tmp/SHA256SUMS" | awk '{print $1}' || true)"
[[ -n "$expected" ]] || { echo "checksum for $asset not found in release." >&2; exit 1; }
actual="$(sha256 "$tmp/$asset")"
[[ "$expected" == "$actual" ]] || { echo "checksum mismatch for $asset." >&2; exit 1; }
tar -xzf "$tmp/$asset" -C "$tmp"
mv "$tmp/ely-eye-mcp" "$BINARY"
chmod +x "$BINARY"
}
acquire() {
case "$METHOD" in
source) build_from_source; return ;;
download)
local tag; tag="$(resolve_version)"
[[ -n "$tag" ]] || { echo "no prebuilt release found for ${REPO}." >&2; exit 1; }
download_binary "$tag"; return ;;
auto)
local tag; tag="$(resolve_version)"
if [[ -n "$tag" ]]; then
download_binary "$tag"
else
echo "No prebuilt release available; building from source."
build_from_source
fi ;;
esac
}
acquire
"$BINARY" setup \
--client "$CLIENT" \
--scope "$SCOPE" \
--server-name "$SERVER_NAME" \
--project-root "$PROJECT_ROOT" \
--ely-eye-home "$ELY_EYE_HOME" \
--binary "$BINARY"
+128
View File
@@ -0,0 +1,128 @@
package cli
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"os"
"github.com/ZacharyZhang-NY/ely-eye/ely-eye-mcp/internal/config"
"github.com/ZacharyZhang-NY/ely-eye/ely-eye-mcp/internal/mcpserver"
"github.com/ZacharyZhang-NY/ely-eye/ely-eye-mcp/internal/setup"
"github.com/ZacharyZhang-NY/ely-eye/ely-eye-mcp/internal/store"
)
// version is stamped at release build time via -ldflags -X. The default is the
// development version used by source builds.
var version = "0.1.0"
func Run(ctx context.Context, args []string) error {
if len(args) == 0 {
return errors.New(usage())
}
switch args[0] {
case "server":
return runServer(ctx, args[1:])
case "setup":
return runSetup(ctx, args[1:])
case "status":
return runStatus(ctx, args[1:])
case "version":
fmt.Println(version)
return nil
case "help", "-h", "--help":
fmt.Print(usage())
return nil
default:
return errors.New("unknown command: " + args[0])
}
}
func runServer(ctx context.Context, args []string) error {
flags := flag.NewFlagSet("server", flag.ContinueOnError)
projectRoot := flags.String("project-root", "", "Ely-Eye project root")
elyEyeHome := flags.String("ely-eye-home", "", "Ely-Eye data home")
flags.SetOutput(os.Stderr)
if err := flags.Parse(args); err != nil {
return err
}
cfg, err := config.Resolve(*projectRoot, *elyEyeHome, "")
if err != nil {
return err
}
return mcpserver.Run(ctx, cfg, version)
}
func runSetup(ctx context.Context, args []string) error {
flags := flag.NewFlagSet("setup", flag.ContinueOnError)
projectRoot := flags.String("project-root", "", "Ely-Eye project root")
elyEyeHome := flags.String("ely-eye-home", "", "Ely-Eye data home")
binary := flags.String("binary", "", "MCP server binary path")
client := flags.String("client", "both", "codex, claude, or both")
scope := flags.String("scope", "project", "Claude scope: project or user")
serverName := flags.String("server-name", "ely-eye", "MCP server name")
flags.SetOutput(os.Stderr)
if err := flags.Parse(args); err != nil {
return err
}
cfg, err := config.Resolve(*projectRoot, *elyEyeHome, *binary)
if err != nil {
return err
}
result, err := setup.Run(ctx, cfg, setup.Options{
Client: *client,
Scope: *scope,
ServerName: *serverName,
})
if err != nil {
return err
}
return printJSON(result)
}
func runStatus(ctx context.Context, args []string) error {
flags := flag.NewFlagSet("status", flag.ContinueOnError)
projectRoot := flags.String("project-root", "", "Ely-Eye project root")
elyEyeHome := flags.String("ely-eye-home", "", "Ely-Eye data home")
flags.SetOutput(os.Stderr)
if err := flags.Parse(args); err != nil {
return err
}
cfg, err := config.Resolve(*projectRoot, *elyEyeHome, "")
if err != nil {
return err
}
db, err := store.Open(ctx, cfg)
if err != nil {
return err
}
defer db.Close()
status, err := db.Status(ctx)
if err != nil {
return err
}
return printJSON(status)
}
func printJSON(value any) error {
encoded, err := json.MarshalIndent(value, "", " ")
if err != nil {
return err
}
fmt.Println(string(encoded))
return nil
}
func usage() string {
return `ely-eye-mcp
Commands:
server Run the MCP server over stdio
setup Register the MCP server with Codex and Claude Code
status Print read-only Ely-Eye memory status
version Print the binary version
`
}
+87
View File
@@ -0,0 +1,87 @@
package config
import (
"errors"
"os"
"path/filepath"
)
type Config struct {
ProjectRoot string `json:"project_root"`
ElyEyeHome string `json:"ely_eye_home"`
Database string `json:"database"`
Binary string `json:"binary"`
}
// Resolve locates the Ely-Eye data home and its SQLite database. The server is
// a read-only mirror of any local .ely_eye store, so identity is anchored on the
// database itself rather than on Ely-Eye repository layout. This lets the MCP
// server be deployed for any project that owns an .ely_eye/data/ely_eye.sqlite.
func Resolve(projectRoot string, elyEyeHome string, binary string) (Config, error) {
home, err := resolveHome(projectRoot, elyEyeHome)
if err != nil {
return Config{}, err
}
db := filepath.Join(home, "data", "ely_eye.sqlite")
if _, err := os.Stat(db); err != nil {
return Config{}, errors.New("Ely-Eye database is required at " + db)
}
root := projectRoot
if root == "" {
root = filepath.Dir(home)
}
root, err = filepath.Abs(root)
if err != nil {
return Config{}, err
}
if binary == "" {
binary, _ = os.Executable()
}
if binary != "" {
binary, err = filepath.Abs(binary)
if err != nil {
return Config{}, err
}
}
return Config{
ProjectRoot: root,
ElyEyeHome: home,
Database: db,
Binary: binary,
}, nil
}
func resolveHome(projectRoot string, elyEyeHome string) (string, error) {
if elyEyeHome == "" {
elyEyeHome = os.Getenv("ELY_EYE_HOME")
}
if elyEyeHome != "" {
return filepath.Abs(elyEyeHome)
}
if projectRoot != "" {
abs, err := filepath.Abs(projectRoot)
if err != nil {
return "", err
}
return filepath.Join(abs, ".ely_eye"), nil
}
return discoverHome()
}
func discoverHome() (string, error) {
current, err := os.Getwd()
if err != nil {
return "", err
}
for {
home := filepath.Join(current, ".ely_eye")
if _, err := os.Stat(filepath.Join(home, "data", "ely_eye.sqlite")); err == nil {
return home, nil
}
next := filepath.Dir(current)
if next == current {
return "", errors.New("cannot find an Ely-Eye data home; pass --ely-eye-home or set ELY_EYE_HOME")
}
current = next
}
}
@@ -0,0 +1,175 @@
package contextpack
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
"unicode/utf8"
"github.com/ZacharyZhang-NY/ely-eye/ely-eye-mcp/internal/domain"
)
var profileBudgets = map[string]int64{
"live_demo": 128000,
"extreme_context": 1010000,
"library_100m": 262144,
"research_theater": 128000,
}
func Build(question string, profile string, hits []domain.AtomHit, requestedBudget int64) domain.EvidencePack {
if profile == "" {
profile = "library_100m"
}
budget := requestedBudget
if budget <= 0 {
budget = profileBudgets[profile]
}
if budget <= 0 {
budget = profileBudgets["library_100m"]
}
var sections []string
var tokens int64
for _, hit := range hits {
text := focusText(hit.Atom.Text, question, 1600)
atomTokens := tokenEquivalent(text)
if atomTokens == 0 {
atomTokens = 1
}
if tokens+atomTokens > budget {
break
}
sections = append(sections, evidenceSection(hit, text))
tokens += atomTokens
}
trace := traceID(question, profile, tokens, hits)
return domain.EvidencePack{
Question: question,
Profile: profile,
TokenBudget: budget,
TokenEquivalent: tokens,
CacheTraceID: trace,
Hits: SummarizeHits(hits, 900),
PackedText: strings.Join(sections, "\n\n"),
VerifierContract: []string{
"Every factual claim must cite atom ids.",
"Citations must include page, image region, frame, or code path when available.",
"Contradictions must be reported as contradiction_notes.",
},
}
}
func SummarizeHits(hits []domain.AtomHit, maxRunes int) []domain.AtomSearchHit {
summaries := make([]domain.AtomSearchHit, 0, len(hits))
for _, hit := range hits {
summaries = append(summaries, domain.AtomSearchHit{
Atom: summarizeAtom(hit.Atom, maxRunes),
SparseScore: hit.SparseScore,
FinalScore: hit.FinalScore,
})
}
return summaries
}
func summarizeAtom(atom domain.Atom, maxRunes int) domain.AtomSummary {
return domain.AtomSummary{
AtomID: atom.AtomID,
SourceID: atom.SourceID,
Modality: atom.Modality,
Source: atom.Source,
Time: atom.Time,
TextPreview: trimRunes(atom.Text, maxRunes),
ImageRef: atom.ImageRef,
TokenEquivalent: atom.TokenEquivalent,
}
}
func evidenceSection(hit domain.AtomHit, text string) string {
return fmt.Sprintf(
"[EVIDENCE]\natom_id=%s\nmodality=%s\nsource=%s\nimage_ref=%s\nscore=%.4f\n%s\n[/EVIDENCE]",
hit.Atom.AtomID,
hit.Atom.Modality,
hit.Atom.Source,
hit.Atom.ImageRef,
hit.FinalScore,
text,
)
}
func focusText(text string, query string, window int) string {
if utf8.RuneCountInString(text) <= window {
return text
}
lowerText := strings.ToLower(text)
best := 0
for _, term := range strings.Fields(strings.ToLower(query)) {
if len(term) < 3 {
continue
}
if index := strings.Index(lowerText, term); index >= 0 {
best = index
break
}
}
start := best - window/2
if start < 0 {
start = 0
}
runes := []rune(text)
runeStart := byteToRuneOffset(text, start)
if runeStart+window > len(runes) {
runeStart = len(runes) - window
}
if runeStart < 0 {
runeStart = 0
}
return string(runes[runeStart : runeStart+window])
}
func trimRunes(text string, limit int) string {
if limit <= 0 || utf8.RuneCountInString(text) <= limit {
return text
}
runes := []rune(text)
return string(runes[:limit])
}
func tokenEquivalent(text string) int64 {
var ascii int64
var nonASCII int64
for _, r := range text {
if r < 128 {
ascii++
} else {
nonASCII++
}
}
total := ascii/4 + nonASCII/2
if total == 0 && text != "" {
return 1
}
return total
}
func traceID(question string, profile string, tokens int64, hits []domain.AtomHit) string {
hash := sha256.New()
fmt.Fprintf(hash, "%s\n%s\n%d\n", question, profile, tokens)
for _, hit := range hits {
fmt.Fprintln(hash, hit.Atom.AtomID)
}
return hex.EncodeToString(hash.Sum(nil))[:16]
}
func byteToRuneOffset(text string, byteOffset int) int {
if byteOffset <= 0 {
return 0
}
count := 0
for index := range text {
if index >= byteOffset {
return count
}
count++
}
return count
}
+148
View File
@@ -0,0 +1,148 @@
package domain
type CacheLayer struct {
Layer string `json:"layer"`
Entries int `json:"entries"`
BytesEstimate int64 `json:"bytes_estimate"`
HitCount int `json:"hit_count"`
MissCount int `json:"miss_count"`
LastEventAt string `json:"last_event_at,omitempty"`
}
type Status struct {
App string `json:"app"`
ProjectRoot string `json:"project_root"`
DataHome string `json:"data_home"`
Database string `json:"database"`
AtomCount int `json:"atom_count"`
SourceCount int `json:"source_count"`
CartridgeCount int `json:"cartridge_count"`
ActiveTokenEquivalent int64 `json:"active_token_equivalent"`
LibraryTokenEquivalent int64 `json:"library_token_equivalent"`
CacheLayers []CacheLayer `json:"cache_layers"`
}
type Cartridge struct {
CartridgeID string `json:"cartridge_id"`
Name string `json:"name"`
RootPath string `json:"root_path"`
DNA string `json:"dna,omitempty"`
TokenEquivalent int64 `json:"token_equivalent"`
CreatedAt string `json:"created_at"`
Manifest any `json:"manifest,omitempty"`
}
type CartridgeDetail struct {
Cartridge
Artifacts map[string]string `json:"artifacts"`
MemoryCapsuleIndex any `json:"memory_capsule_index,omitempty"`
AssetReport any `json:"asset_report,omitempty"`
}
type Atom struct {
AtomID string `json:"atom_id"`
SourceID string `json:"source_id"`
Modality string `json:"modality"`
Source string `json:"source"`
Time string `json:"time"`
Text string `json:"text"`
ImageRef string `json:"image_ref,omitempty"`
Layout any `json:"layout,omitempty"`
Relations any `json:"relations,omitempty"`
Trust any `json:"trust,omitempty"`
TokenEquivalent int64 `json:"token_equivalent"`
Metadata any `json:"metadata,omitempty"`
}
type AtomHit struct {
Atom Atom `json:"atom"`
SparseScore float64 `json:"sparse_score"`
FinalScore float64 `json:"final_score"`
}
type AtomSummary struct {
AtomID string `json:"atom_id"`
SourceID string `json:"source_id"`
Modality string `json:"modality"`
Source string `json:"source"`
Time string `json:"time"`
TextPreview string `json:"text_preview"`
ImageRef string `json:"image_ref,omitempty"`
TokenEquivalent int64 `json:"token_equivalent"`
}
type AtomSearchHit struct {
Atom AtomSummary `json:"atom"`
SparseScore float64 `json:"sparse_score"`
FinalScore float64 `json:"final_score"`
}
type EvidencePack struct {
Question string `json:"question"`
Profile string `json:"profile"`
TokenBudget int64 `json:"token_budget"`
TokenEquivalent int64 `json:"token_equivalent"`
CacheTraceID string `json:"cache_trace_id"`
Hits []AtomSearchHit `json:"hits"`
PackedText string `json:"packed_text"`
VerifierContract []string `json:"verifier_contract"`
}
type ProofSuite struct {
ProofID string `json:"proof_id"`
CartridgeID string `json:"cartridge_id"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
Path string `json:"path"`
Payload any `json:"payload,omitempty"`
}
type HashHopProof struct {
ProofID string `json:"proof_id"`
Kind string `json:"kind"`
Hops int `json:"hops"`
TokenEquivalent int64 `json:"token_equivalent"`
QueryID string `json:"query_id"`
ExpectedTargetID string `json:"expected_target_id"`
ModelTargetID string `json:"model_target_id,omitempty"`
Passed *bool `json:"passed"`
Artifacts []string `json:"artifacts,omitempty"`
CreatedAt string `json:"created_at"`
Path string `json:"path"`
}
type HashHopBucket struct {
Kind string `json:"kind"`
Hops int `json:"hops"`
TokenEquivalent int64 `json:"token_equivalent"`
Total int `json:"total"`
Passed int `json:"passed"`
}
type HashHopReport struct {
Total int `json:"total"`
Passed int `json:"passed"`
Buckets []HashHopBucket `json:"buckets"`
Proofs []HashHopProof `json:"proofs,omitempty"`
}
type ProofCheck struct {
Name string `json:"name"`
Requirement string `json:"requirement"`
Status string `json:"status"`
Evidence []string `json:"evidence,omitempty"`
Detail string `json:"detail"`
}
type ProofSuiteDetail struct {
ProofID string `json:"proof_id"`
CartridgeID string `json:"cartridge_id"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
Checks []ProofCheck `json:"checks"`
Artifacts map[string]string `json:"artifacts"`
DNABefore string `json:"dna_before,omitempty"`
DNAAfter string `json:"dna_after,omitempty"`
Path string `json:"path"`
HashHop HashHopReport `json:"hashhop"`
}
+83
View File
@@ -0,0 +1,83 @@
package mcpserver
import (
"context"
"errors"
"io"
"os"
"path/filepath"
"strings"
"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
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, 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. Tools read local Ely-Eye state and do not mutate the production database.",
})
server.registerTools(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
})
}
+212
View File
@@ -0,0 +1,212 @@
package mcpserver
import (
"context"
"errors"
"github.com/ZacharyZhang-NY/ely-eye/ely-eye-mcp/internal/contextpack"
"github.com/ZacharyZhang-NY/ely-eye/ely-eye-mcp/internal/domain"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
type EmptyInput struct{}
type ListCartridgesInput struct {
Limit int `json:"limit,omitempty" jsonschema:"maximum number of cartridges to return, capped at 200"`
}
type GetCartridgeInput struct {
CartridgeID string `json:"cartridge_id,omitempty" jsonschema:"exact Ely-Eye cartridge id"`
Name string `json:"name,omitempty" jsonschema:"exact Ely-Eye cartridge name"`
}
type SearchAtomsInput struct {
Query string `json:"query" jsonschema:"search query for the Ely-Eye FTS atom index"`
Modality string `json:"modality,omitempty" jsonschema:"optional exact modality filter such as text, code, image, pdf_page, pdf_region, video_frame, ui_screenshot"`
Limit int `json:"limit,omitempty" jsonschema:"maximum number of atoms to return, capped at 100"`
}
type FetchAtomInput struct {
AtomID string `json:"atom_id" jsonschema:"exact Evidence Atom id"`
}
type CompileContextInput struct {
Question string `json:"question" jsonschema:"question to compile into an Ely-Eye evidence pack"`
Profile string `json:"profile,omitempty" jsonschema:"live_demo, extreme_context, library_100m, or research_theater"`
Limit int `json:"limit,omitempty" jsonschema:"maximum retrieval hits to pack"`
TokenBudget int64 `json:"token_budget,omitempty" jsonschema:"explicit token budget for the evidence pack"`
}
type ListProofSuitesInput struct {
Limit int `json:"limit,omitempty" jsonschema:"maximum proof suites to return, capped at 100"`
IncludePayload bool `json:"include_payload,omitempty" jsonschema:"include full proof_suite.json payloads"`
}
type ListHashHopProofsInput struct {
Kind string `json:"kind,omitempty" jsonschema:"optional filter: text_hashhop or visual_hashhop"`
Limit int `json:"limit,omitempty" jsonschema:"maximum example proofs to include, capped at 200"`
}
type GetProofSuiteInput struct {
ProofID string `json:"proof_id" jsonschema:"exact proof suite id, for example prd_proof_suite_6bc3fa744c60a0e6"`
}
type StatusOutput struct {
Status domain.Status `json:"status"`
}
type ListCartridgesOutput struct {
Cartridges []domain.Cartridge `json:"cartridges"`
}
type GetCartridgeOutput struct {
Cartridge domain.CartridgeDetail `json:"cartridge"`
}
type SearchAtomsOutput struct {
Hits []domain.AtomSearchHit `json:"hits"`
}
type FetchAtomOutput struct {
Atom domain.Atom `json:"atom"`
}
type CompileContextOutput struct {
Context domain.EvidencePack `json:"context"`
}
type ListProofSuitesOutput struct {
ProofSuites []domain.ProofSuite `json:"proof_suites"`
}
type ListHashHopProofsOutput struct {
Report domain.HashHopReport `json:"report"`
}
type GetProofSuiteOutput struct {
ProofSuite domain.ProofSuiteDetail `json:"proof_suite"`
}
func (s *Server) registerTools(server *mcp.Server) {
mcp.AddTool(server, &mcp.Tool{
Name: "ely_eye_status",
Title: "Ely-Eye status",
Description: "Return read-only counts and cache layer status from the local Ely-Eye database.",
}, s.statusTool)
mcp.AddTool(server, &mcp.Tool{
Name: "list_cartridges",
Title: "List Context Cartridges",
Description: "List local Ely-Eye Context Cartridges with DNA, token-equivalent capacity, and manifest JSON.",
}, s.listCartridgesTool)
mcp.AddTool(server, &mcp.Tool{
Name: "get_cartridge",
Title: "Get Context Cartridge",
Description: "Read one local Ely-Eye Context Cartridge, including artifacts, memory capsule index, and asset report when present.",
}, s.getCartridgeTool)
mcp.AddTool(server, &mcp.Tool{
Name: "search_atoms",
Title: "Search Evidence Atoms",
Description: "Search the local Ely-Eye FTS index and return ranked Evidence Atoms.",
}, s.searchAtomsTool)
mcp.AddTool(server, &mcp.Tool{
Name: "fetch_atom",
Title: "Fetch Evidence Atom",
Description: "Read one Evidence Atom by exact atom id.",
}, s.fetchAtomTool)
mcp.AddTool(server, &mcp.Tool{
Name: "compile_context",
Title: "Compile Evidence Context",
Description: "Compile a question into an Ely-Eye evidence pack using local retrieval and the same profile names as the app.",
}, s.compileContextTool)
mcp.AddTool(server, &mcp.Tool{
Name: "list_proof_suites",
Title: "List Proof Suites",
Description: "List local PRD proof suites from .ely_eye/data/eval_proofs.",
}, s.listProofSuitesTool)
mcp.AddTool(server, &mcp.Tool{
Name: "list_hashhop_proofs",
Title: "List HashHop Proofs",
Description: "Aggregate local HashHop and Visual HashHop long-context addressing proofs by kind, hop count, and token budget, with pass counts and example proofs.",
}, s.listHashHopProofsTool)
mcp.AddTool(server, &mcp.Tool{
Name: "get_proof_suite",
Title: "Get Proof Suite",
Description: "Read one local PRD proof suite by id, including all checks and a summary of its HashHop proofs.",
}, s.getProofSuiteTool)
}
func (s *Server) statusTool(ctx context.Context, _ *mcp.CallToolRequest, _ EmptyInput) (*mcp.CallToolResult, StatusOutput, error) {
status, err := s.store.Status(ctx)
if err != nil {
return nil, StatusOutput{}, err
}
return nil, StatusOutput{Status: status}, nil
}
func (s *Server) listCartridgesTool(ctx context.Context, _ *mcp.CallToolRequest, input ListCartridgesInput) (*mcp.CallToolResult, ListCartridgesOutput, error) {
cartridges, err := s.store.ListCartridges(ctx, input.Limit)
if err != nil {
return nil, ListCartridgesOutput{}, err
}
return nil, ListCartridgesOutput{Cartridges: cartridges}, nil
}
func (s *Server) getCartridgeTool(ctx context.Context, _ *mcp.CallToolRequest, input GetCartridgeInput) (*mcp.CallToolResult, GetCartridgeOutput, error) {
cartridge, err := s.store.GetCartridge(ctx, input.CartridgeID, input.Name)
if err != nil {
return nil, GetCartridgeOutput{}, err
}
return nil, GetCartridgeOutput{Cartridge: cartridge}, nil
}
func (s *Server) searchAtomsTool(ctx context.Context, _ *mcp.CallToolRequest, input SearchAtomsInput) (*mcp.CallToolResult, SearchAtomsOutput, error) {
hits, err := s.store.SearchAtoms(ctx, input.Query, input.Modality, input.Limit)
if err != nil {
return nil, SearchAtomsOutput{}, err
}
return nil, SearchAtomsOutput{Hits: contextpack.SummarizeHits(hits, 1200)}, nil
}
func (s *Server) fetchAtomTool(ctx context.Context, _ *mcp.CallToolRequest, input FetchAtomInput) (*mcp.CallToolResult, FetchAtomOutput, error) {
atom, err := s.store.FetchAtom(ctx, input.AtomID)
if err != nil {
return nil, FetchAtomOutput{}, err
}
return nil, FetchAtomOutput{Atom: atom}, nil
}
func (s *Server) compileContextTool(ctx context.Context, _ *mcp.CallToolRequest, input CompileContextInput) (*mcp.CallToolResult, CompileContextOutput, error) {
if input.Question == "" {
return nil, CompileContextOutput{}, errors.New("question is required")
}
hits, err := s.store.SearchAtoms(ctx, input.Question, "", input.Limit)
if err != nil {
return nil, CompileContextOutput{}, err
}
pack := contextpack.Build(input.Question, input.Profile, hits, input.TokenBudget)
return nil, CompileContextOutput{Context: pack}, nil
}
func (s *Server) listProofSuitesTool(ctx context.Context, _ *mcp.CallToolRequest, input ListProofSuitesInput) (*mcp.CallToolResult, ListProofSuitesOutput, error) {
proofs, err := s.store.ListProofSuites(ctx, input.Limit, input.IncludePayload)
if err != nil {
return nil, ListProofSuitesOutput{}, err
}
return nil, ListProofSuitesOutput{ProofSuites: proofs}, nil
}
func (s *Server) listHashHopProofsTool(ctx context.Context, _ *mcp.CallToolRequest, input ListHashHopProofsInput) (*mcp.CallToolResult, ListHashHopProofsOutput, error) {
report, err := s.store.ListHashHopProofs(ctx, input.Kind, input.Limit)
if err != nil {
return nil, ListHashHopProofsOutput{}, err
}
return nil, ListHashHopProofsOutput{Report: report}, nil
}
func (s *Server) getProofSuiteTool(ctx context.Context, _ *mcp.CallToolRequest, input GetProofSuiteInput) (*mcp.CallToolResult, GetProofSuiteOutput, error) {
suite, err := s.store.GetProofSuite(ctx, input.ProofID)
if err != nil {
return nil, GetProofSuiteOutput{}, err
}
return nil, GetProofSuiteOutput{ProofSuite: suite}, nil
}
+220
View File
@@ -0,0 +1,220 @@
package setup
import (
"context"
"encoding/json"
"errors"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/ZacharyZhang-NY/ely-eye/ely-eye-mcp/internal/config"
)
type Options struct {
Client string
Scope string
ServerName string
}
type Action struct {
Client string `json:"client"`
Scope string `json:"scope"`
Target string `json:"target"`
Detail string `json:"detail"`
Command string `json:"command,omitempty"`
}
type Result struct {
ServerName string `json:"server_name"`
Binary string `json:"binary"`
ProjectRoot string `json:"project_root"`
Actions []Action `json:"actions"`
}
func Run(ctx context.Context, cfg config.Config, options Options) (Result, error) {
client := normalizeClient(options.Client)
if client != "codex" && client != "claude" && client != "both" {
return Result{}, errors.New("client must be codex, claude, or both")
}
scope := normalizeScope(options.Scope)
if scope != "project" && scope != "user" {
return Result{}, errors.New("claude scope must be project or user")
}
serverName := options.ServerName
if serverName == "" {
serverName = "ely-eye"
}
if cfg.Binary == "" {
return Result{}, errors.New("binary path is required for setup")
}
result := Result{ServerName: serverName, Binary: cfg.Binary, ProjectRoot: cfg.ProjectRoot}
var actions []Action
if client == "codex" || client == "both" {
action, err := setupCodex(ctx, cfg, serverName)
if err != nil {
return Result{}, err
}
actions = append(actions, action)
}
if client == "claude" || client == "both" {
action, err := setupClaude(ctx, cfg, serverName, scope)
if err != nil {
return Result{}, err
}
actions = append(actions, action)
}
result.Actions = actions
return result, nil
}
func normalizeClient(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "", "all", "both":
return "both"
case "codex", "claude":
return strings.ToLower(value)
default:
return value
}
}
func normalizeScope(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "", "project":
return "project"
case "user":
return "user"
default:
return value
}
}
func serverCommand(cfg config.Config) []string {
return []string{
cfg.Binary,
"server",
"--project-root",
cfg.ProjectRoot,
"--ely-eye-home",
cfg.ElyEyeHome,
}
}
func serverEnv(cfg config.Config) map[string]string {
return map[string]string{
"ELY_EYE_HOME": cfg.ElyEyeHome,
}
}
func commandLine(parts []string) string {
quoted := make([]string, 0, len(parts))
for _, part := range parts {
if strings.ContainsAny(part, " \t\"'") {
quoted = append(quoted, `"`+strings.ReplaceAll(part, `"`, `\"`)+`"`)
} else {
quoted = append(quoted, part)
}
}
return strings.Join(quoted, " ")
}
func setupCodex(ctx context.Context, cfg config.Config, serverName string) (Action, error) {
codex, err := exec.LookPath("codex")
if err != nil {
return Action{}, errors.New("codex CLI is required to configure Codex MCP")
}
remove := exec.CommandContext(ctx, codex, "mcp", "remove", serverName)
_ = remove.Run()
args := []string{"mcp", "add", serverName, "--env", "ELY_EYE_HOME=" + cfg.ElyEyeHome, "--"}
args = append(args, serverCommand(cfg)...)
add := exec.CommandContext(ctx, codex, args...)
output, err := add.CombinedOutput()
if err != nil {
return Action{}, errors.New("codex mcp add failed: " + strings.TrimSpace(string(output)))
}
return Action{
Client: "codex",
Scope: "user",
Target: "~/.codex/config.toml",
Detail: "registered stdio MCP server through Codex CLI",
Command: commandLine(append([]string{"codex"}, args...)),
}, nil
}
func setupClaude(ctx context.Context, cfg config.Config, serverName string, scope string) (Action, error) {
if scope == "user" {
return setupClaudeUser(ctx, cfg, serverName)
}
if scope != "project" {
return Action{}, errors.New("claude scope must be project or user")
}
target := filepath.Join(cfg.ProjectRoot, ".mcp.json")
if err := writeProjectMCP(target, serverName, cfg); err != nil {
return Action{}, err
}
return Action{
Client: "claude",
Scope: "project",
Target: target,
Detail: "wrote project-scoped .mcp.json",
}, nil
}
func setupClaudeUser(ctx context.Context, cfg config.Config, serverName string) (Action, error) {
claude, err := exec.LookPath("claude")
if err != nil {
return Action{}, errors.New("claude CLI is required for user-scoped Claude Code MCP setup")
}
payload, err := json.Marshal(mcpServerConfig(cfg))
if err != nil {
return Action{}, err
}
args := []string{"mcp", "add-json", serverName, string(payload), "--scope", "user"}
cmd := exec.CommandContext(ctx, claude, args...)
output, err := cmd.CombinedOutput()
if err != nil {
return Action{}, errors.New("claude mcp add-json failed: " + strings.TrimSpace(string(output)))
}
return Action{
Client: "claude",
Scope: "user",
Target: "~/.claude.json",
Detail: "registered stdio MCP server through Claude CLI",
Command: commandLine(append([]string{"claude"}, args...)),
}, nil
}
func writeProjectMCP(path string, serverName string, cfg config.Config) error {
payload := map[string]any{}
data, err := os.ReadFile(path)
if err == nil {
if err := json.Unmarshal(data, &payload); err != nil {
return err
}
} else if !errors.Is(err, os.ErrNotExist) {
return err
}
servers, ok := payload["mcpServers"].(map[string]any)
if !ok {
servers = map[string]any{}
}
servers[serverName] = mcpServerConfig(cfg)
payload["mcpServers"] = servers
data, err = json.MarshalIndent(payload, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, append(data, '\n'), 0644)
}
func mcpServerConfig(cfg config.Config) map[string]any {
command := serverCommand(cfg)
return map[string]any{
"type": "stdio",
"command": command[0],
"args": command[1:],
"env": serverEnv(cfg),
}
}
+200
View File
@@ -0,0 +1,200 @@
package store
import (
"context"
"database/sql"
"errors"
"math"
"regexp"
"strings"
"github.com/ZacharyZhang-NY/ely-eye/ely-eye-mcp/internal/domain"
)
var tokenPattern = regexp.MustCompile(`[\p{L}\p{N}_]+`)
func (s *Store) FetchAtom(ctx context.Context, atomID string) (domain.Atom, error) {
if strings.TrimSpace(atomID) == "" {
return domain.Atom{}, errors.New("atom_id is required")
}
row := s.db.QueryRowContext(ctx, `
SELECT atom_id, source_id, modality, source, time, text, COALESCE(image_ref, ''),
COALESCE(layout_json, ''), relations_json, trust_json, token_equivalent, metadata_json
FROM atoms
WHERE atom_id = ?`, atomID)
atom, err := scanAtom(row)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return domain.Atom{}, errors.New("atom not found")
}
return domain.Atom{}, err
}
return atom, nil
}
func (s *Store) SearchAtoms(ctx context.Context, query string, modality string, limit int) ([]domain.AtomHit, error) {
matchQuery, err := ftsQuery(query)
if err != nil {
return nil, err
}
if limit <= 0 || limit > 100 {
limit = 24
}
args := []any{matchQuery}
filter := ""
if modality != "" {
filter = "AND a.modality = ?"
args = append(args, modality)
}
args = append(args, limit)
rows, err := s.db.QueryContext(ctx, `
SELECT a.atom_id, a.source_id, a.modality, a.source, a.time, a.text,
COALESCE(a.image_ref, ''), COALESCE(a.layout_json, ''),
a.relations_json, a.trust_json, a.token_equivalent, a.metadata_json,
bm25(atom_fts) AS score
FROM atom_fts
JOIN atoms a ON a.atom_id = atom_fts.atom_id
WHERE atom_fts MATCH ? `+filter+`
ORDER BY score
LIMIT ?`, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var hits []domain.AtomHit
var bestScore float64
for rows.Next() {
atom, score, err := scanAtomHit(rows)
if err != nil {
return nil, err
}
sparseScore := -score
if math.IsNaN(sparseScore) || math.IsInf(sparseScore, 0) {
sparseScore = 0
}
if sparseScore > bestScore {
bestScore = sparseScore
}
hits = append(hits, domain.AtomHit{Atom: atom, SparseScore: sparseScore, FinalScore: sparseScore})
}
if err := rows.Err(); err != nil {
return nil, err
}
if bestScore > 0 {
for index := range hits {
hits[index].SparseScore = hits[index].SparseScore / bestScore
hits[index].FinalScore = hits[index].SparseScore
}
}
return hits, nil
}
type atomScanner interface {
Scan(dest ...any) error
}
func scanAtom(scanner atomScanner) (domain.Atom, error) {
var atom domain.Atom
var layout string
var relations string
var trust string
var metadata string
if err := scanner.Scan(
&atom.AtomID,
&atom.SourceID,
&atom.Modality,
&atom.Source,
&atom.Time,
&atom.Text,
&atom.ImageRef,
&layout,
&relations,
&trust,
&atom.TokenEquivalent,
&metadata,
); err != nil {
return domain.Atom{}, err
}
var err error
atom.Layout, err = optionalJSONValue(layout)
if err != nil {
return domain.Atom{}, err
}
atom.Relations, err = requiredJSONValue(relations)
if err != nil {
return domain.Atom{}, err
}
atom.Trust, err = requiredJSONValue(trust)
if err != nil {
return domain.Atom{}, err
}
atom.Metadata, err = requiredJSONValue(metadata)
if err != nil {
return domain.Atom{}, err
}
return atom, nil
}
func scanAtomHit(scanner atomScanner) (domain.Atom, float64, error) {
var atom domain.Atom
var layout string
var relations string
var trust string
var metadata string
var score float64
if err := scanner.Scan(
&atom.AtomID,
&atom.SourceID,
&atom.Modality,
&atom.Source,
&atom.Time,
&atom.Text,
&atom.ImageRef,
&layout,
&relations,
&trust,
&atom.TokenEquivalent,
&metadata,
&score,
); err != nil {
return domain.Atom{}, 0, err
}
var err error
atom.Layout, err = optionalJSONValue(layout)
if err != nil {
return domain.Atom{}, 0, err
}
atom.Relations, err = requiredJSONValue(relations)
if err != nil {
return domain.Atom{}, 0, err
}
atom.Trust, err = requiredJSONValue(trust)
if err != nil {
return domain.Atom{}, 0, err
}
atom.Metadata, err = requiredJSONValue(metadata)
if err != nil {
return domain.Atom{}, 0, err
}
return atom, score, nil
}
func ftsQuery(query string) (string, error) {
tokens := tokenPattern.FindAllString(strings.ToLower(query), -1)
if len(tokens) == 0 {
return "", errors.New("query must contain searchable text")
}
unique := make([]string, 0, len(tokens))
seen := map[string]bool{}
for _, token := range tokens {
if seen[token] {
continue
}
seen[token] = true
unique = append(unique, `"`+strings.ReplaceAll(token, `"`, `""`)+`"`)
if len(unique) == 16 {
break
}
}
return strings.Join(unique, " OR "), nil
}
+135
View File
@@ -0,0 +1,135 @@
package store
import (
"context"
"database/sql"
"encoding/json"
"errors"
"os"
"path/filepath"
"github.com/ZacharyZhang-NY/ely-eye/ely-eye-mcp/internal/domain"
)
func (s *Store) ListCartridges(ctx context.Context, limit int) ([]domain.Cartridge, error) {
if limit <= 0 || limit > 200 {
limit = 50
}
rows, err := s.db.QueryContext(ctx, `
SELECT cartridge_id, name, root_path, manifest_json, COALESCE(dna, ''),
token_equivalent, created_at
FROM cartridges
ORDER BY created_at DESC
LIMIT ?`, limit)
if err != nil {
return nil, err
}
defer rows.Close()
cartridges := []domain.Cartridge{}
for rows.Next() {
cartridge, err := scanCartridge(rows)
if err != nil {
return nil, err
}
cartridges = append(cartridges, cartridge)
}
return cartridges, rows.Err()
}
func (s *Store) GetCartridge(ctx context.Context, cartridgeID string, name string) (domain.CartridgeDetail, error) {
if cartridgeID == "" && name == "" {
return domain.CartridgeDetail{}, errors.New("cartridge_id or name is required")
}
query := `
SELECT cartridge_id, name, root_path, manifest_json, COALESCE(dna, ''),
token_equivalent, created_at
FROM cartridges
WHERE cartridge_id = ? OR name = ?
ORDER BY created_at DESC
LIMIT 1`
row := s.db.QueryRowContext(ctx, query, cartridgeID, name)
cartridge, err := scanCartridge(row)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return domain.CartridgeDetail{}, errors.New("cartridge not found")
}
return domain.CartridgeDetail{}, err
}
artifacts, err := artifactsFromManifest(cartridge.Manifest)
if err != nil {
return domain.CartridgeDetail{}, err
}
detail := domain.CartridgeDetail{
Cartridge: cartridge,
Artifacts: artifacts,
}
memoryIndex, err := readOptionalJSON(filepath.Join(cartridge.RootPath, "memory_capsule_index.json"))
if err != nil {
return domain.CartridgeDetail{}, err
}
detail.MemoryCapsuleIndex = memoryIndex
assetReport, err := readOptionalJSON(filepath.Join(cartridge.RootPath, "cartridge_assets.json"))
if err != nil {
return domain.CartridgeDetail{}, err
}
detail.AssetReport = assetReport
return detail, nil
}
type cartridgeScanner interface {
Scan(dest ...any) error
}
func scanCartridge(scanner cartridgeScanner) (domain.Cartridge, error) {
var cartridge domain.Cartridge
var manifest string
if err := scanner.Scan(
&cartridge.CartridgeID,
&cartridge.Name,
&cartridge.RootPath,
&manifest,
&cartridge.DNA,
&cartridge.TokenEquivalent,
&cartridge.CreatedAt,
); err != nil {
return domain.Cartridge{}, err
}
value, err := requiredJSONValue(manifest)
if err != nil {
return domain.Cartridge{}, err
}
cartridge.Manifest = value
return cartridge, nil
}
func artifactsFromManifest(manifest any) (map[string]string, error) {
var payload struct {
Artifacts map[string]string `json:"artifacts"`
}
data, err := json.Marshal(manifest)
if err != nil {
return nil, err
}
if err := json.Unmarshal(data, &payload); err != nil {
return nil, err
}
if payload.Artifacts == nil {
payload.Artifacts = map[string]string{}
}
return payload.Artifacts, nil
}
func readOptionalJSON(path string) (any, error) {
data, err := os.ReadFile(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}
return nil, err
}
value, err := readJSONValue(data)
if err != nil {
return nil, errors.New(err.Error() + " at " + path)
}
return value, nil
}
+35
View File
@@ -0,0 +1,35 @@
package store
import (
"encoding/json"
"errors"
)
func optionalJSONValue(value string) (any, error) {
if value == "" || value == "null" {
return nil, nil
}
return decodeJSONValue(value)
}
func requiredJSONValue(value string) (any, error) {
if value == "" {
return map[string]any{}, nil
}
return decodeJSONValue(value)
}
func readJSONValue(data []byte) (any, error) {
if !json.Valid(data) {
return nil, errors.New("invalid JSON")
}
var value any
if err := json.Unmarshal(data, &value); err != nil {
return nil, err
}
return value, nil
}
func decodeJSONValue(value string) (any, error) {
return readJSONValue([]byte(value))
}
@@ -0,0 +1,167 @@
package store
import (
"context"
"encoding/json"
"errors"
"io/fs"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/ZacharyZhang-NY/ely-eye/ely-eye-mcp/internal/domain"
)
func (s *Store) evalProofsRoot() string {
return filepath.Join(s.cfg.ElyEyeHome, "data", "eval_proofs")
}
// ListHashHopProofs aggregates every HashHop and Visual HashHop proof under the
// eval set into pass-rate buckets keyed by kind, hop count, and token budget,
// plus a capped list of recent example proofs.
func (s *Store) ListHashHopProofs(ctx context.Context, kind string, limit int) (domain.HashHopReport, error) {
if kind != "" && kind != "text_hashhop" && kind != "visual_hashhop" {
return domain.HashHopReport{}, errors.New("kind must be text_hashhop or visual_hashhop")
}
if limit <= 0 || limit > 200 {
limit = 25
}
return aggregateHashHop(ctx, s.evalProofsRoot(), kind, limit)
}
// GetProofSuite reads one PRD proof suite by id, including all checks, and
// summarizes the HashHop proofs stored alongside it.
func (s *Store) GetProofSuite(ctx context.Context, proofID string) (domain.ProofSuiteDetail, error) {
if err := validateProofID(proofID); err != nil {
return domain.ProofSuiteDetail{}, err
}
dir := filepath.Join(s.evalProofsRoot(), proofID)
path := filepath.Join(dir, "proof_suite.json")
data, err := os.ReadFile(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return domain.ProofSuiteDetail{}, errors.New("proof suite not found")
}
return domain.ProofSuiteDetail{}, err
}
var detail domain.ProofSuiteDetail
if err := json.Unmarshal(data, &detail); err != nil {
return domain.ProofSuiteDetail{}, err
}
detail.Path = path
report, err := aggregateHashHop(ctx, filepath.Join(dir, "hashhop"), "", 0)
if err != nil {
return domain.ProofSuiteDetail{}, err
}
detail.HashHop = report
return detail, nil
}
// aggregateHashHop walks a directory tree for proof.json files and builds a
// HashHopReport. proofCap <= 0 returns buckets only; a positive cap also
// includes that many most-recent example proofs.
func aggregateHashHop(ctx context.Context, root string, kind string, proofCap int) (domain.HashHopReport, error) {
buckets := map[string]*domain.HashHopBucket{}
seen := map[string]bool{}
var proofs []domain.HashHopProof
report := domain.HashHopReport{}
err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if err := ctx.Err(); err != nil {
return err
}
if entry.IsDir() || entry.Name() != "proof.json" {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
var proof domain.HashHopProof
if err := json.Unmarshal(data, &proof); err != nil {
return err
}
if kind != "" && proof.Kind != kind {
return nil
}
// A suite stores each HashHop proof under its own hashhop/ directory and
// also promotes a byte-identical copy to a top-level proof directory.
// Count each proof once by its content-derived id.
if seen[proof.ProofID] {
return nil
}
seen[proof.ProofID] = true
proof.Path = path
report.Total++
passed := proof.Passed != nil && *proof.Passed
if passed {
report.Passed++
}
key := proof.Kind + "|" + strconv.Itoa(proof.Hops) + "|" + strconv.FormatInt(proof.TokenEquivalent, 10)
bucket, ok := buckets[key]
if !ok {
bucket = &domain.HashHopBucket{Kind: proof.Kind, Hops: proof.Hops, TokenEquivalent: proof.TokenEquivalent}
buckets[key] = bucket
}
bucket.Total++
if passed {
bucket.Passed++
}
if proofCap > 0 {
proofs = append(proofs, proof)
}
return nil
})
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return domain.HashHopReport{Buckets: []domain.HashHopBucket{}}, nil
}
return domain.HashHopReport{}, err
}
report.Buckets = sortedBuckets(buckets)
if proofCap > 0 {
report.Proofs = capRecentProofs(proofs, proofCap)
}
return report, nil
}
func sortedBuckets(buckets map[string]*domain.HashHopBucket) []domain.HashHopBucket {
out := make([]domain.HashHopBucket, 0, len(buckets))
for _, bucket := range buckets {
out = append(out, *bucket)
}
sort.Slice(out, func(i int, j int) bool {
if out[i].Kind != out[j].Kind {
return out[i].Kind < out[j].Kind
}
if out[i].TokenEquivalent != out[j].TokenEquivalent {
return out[i].TokenEquivalent < out[j].TokenEquivalent
}
return out[i].Hops < out[j].Hops
})
return out
}
func capRecentProofs(proofs []domain.HashHopProof, limit int) []domain.HashHopProof {
sort.Slice(proofs, func(i int, j int) bool {
return proofs[i].CreatedAt > proofs[j].CreatedAt
})
if len(proofs) > limit {
proofs = proofs[:limit]
}
return proofs
}
func validateProofID(id string) error {
if id == "" {
return errors.New("proof_id is required")
}
if id == "." || id == ".." || strings.ContainsAny(id, `/\`) || filepath.VolumeName(id) != "" || filepath.Base(id) != id {
return errors.New("proof_id must be a single path element")
}
return nil
}
+104
View File
@@ -0,0 +1,104 @@
package store
import (
"context"
"encoding/json"
"errors"
"os"
"path/filepath"
"sort"
"time"
"github.com/ZacharyZhang-NY/ely-eye/ely-eye-mcp/internal/domain"
)
type proofDir struct {
name string
modTime time.Time
}
func (s *Store) ListProofSuites(ctx context.Context, limit int, includePayload bool) ([]domain.ProofSuite, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if limit <= 0 || limit > 100 {
limit = 20
}
root := filepath.Join(s.cfg.ElyEyeHome, "data", "eval_proofs")
entries, err := os.ReadDir(root)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return []domain.ProofSuite{}, nil
}
return nil, err
}
dirs := sortedProofDirs(entries)
proofs := make([]domain.ProofSuite, 0, limit)
for _, dir := range dirs {
if err := ctx.Err(); err != nil {
return nil, err
}
path := filepath.Join(root, dir.name, "proof_suite.json")
data, err := os.ReadFile(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
continue
}
return nil, err
}
var header struct {
ProofID string `json:"proof_id"`
CartridgeID string `json:"cartridge_id"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
}
if err := json.Unmarshal(data, &header); err != nil {
return nil, err
}
proof := domain.ProofSuite{
ProofID: header.ProofID,
CartridgeID: header.CartridgeID,
Status: header.Status,
CreatedAt: header.CreatedAt,
Path: path,
}
if includePayload {
value, err := readJSONValue(data)
if err != nil {
return nil, err
}
proof.Payload = value
}
proofs = append(proofs, proof)
if len(proofs) == limit {
break
}
}
return proofs, nil
}
// sortedProofDirs resolves each directory's mod time once and orders newest
// first. The store mutates underneath the read-only server, so a directory may
// be removed between ReadDir and Info; an Info error yields a zero time that
// sorts last rather than dereferencing a nil FileInfo. Names break ties for a
// stable order.
func sortedProofDirs(entries []os.DirEntry) []proofDir {
dirs := make([]proofDir, 0, len(entries))
for _, entry := range entries {
if !entry.IsDir() {
continue
}
modTime := time.Time{}
if info, err := entry.Info(); err == nil {
modTime = info.ModTime()
}
dirs = append(dirs, proofDir{name: entry.Name(), modTime: modTime})
}
sort.Slice(dirs, func(i int, j int) bool {
if !dirs[i].modTime.Equal(dirs[j].modTime) {
return dirs[i].modTime.After(dirs[j].modTime)
}
return dirs[i].name < dirs[j].name
})
return dirs
}
+136
View File
@@ -0,0 +1,136 @@
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())
}