Files
ely-eye/ely-eye-mcp/internal/mcpserver/tools.go
T
ZacharyZhang-NY 709104e809 Make the speech numbers reproducible instead of merely printed
A review of 5289cb4 found the ASR report was not honest arithmetic:

- "audio s" was the last segment's end, not the media length. The real
  container durations are 88.93 / 109.18 / 60.07 s, so the report understated
  the audio by 48 s and every speed cell was wrong.
- Decoding was unpinned, so a rerun was a new draw: WER moved 0.242 to 0.262
  and one video moved 0.278 to 0.583. Greedy single-temperature decoding with
  no carry-over now answers the same way twice, and the harness runs the pass
  twice to show it.
- The report claimed its WER was an upper bound. It is not a bound in either
  direction: a caption that drops words also hides words the model dropped.
  The denominators (201 / 296 / 36 reference words) are printed instead.
- "66 speech atoms" was bench-side arithmetic. The store now says 80 audio
  atoms carrying 563 word timestamps, read back after a real ingest.

Same silent-skip bug in a second place: a text file over max_text_file_bytes
returned an empty batch with no reason. It raises FileTooLarge with its size,
and a walk records that refusal like any other.

Also fixed from the same review: the Go search tool never published the audio
modality, cli_senses carried a second json_dump, the no-audio-track branch had
a test that fed it markdown, _seconds was an empty wrapper, and the docstring
quoted a speed no report measured.

Gate root cause: bench/tests imports the bench package by name with no root
conftest, so bare pytest could not collect it at all. pythonpath in pyproject
fixes both launch styles; 212 tests pass either way.

Verification: 212 passed (bare pytest and python -m pytest), ruff clean,
mypy clean on the six touched files with a cold cache, repo-wide mypy 24/6
unchanged from HEAD, pyright 0 errors on the touched files, go build/vet/test
and gofmt clean.
2026-10-07 22:23:33 -04:00

222 lines
8.6 KiB
Go

package mcpserver
import (
"context"
"errors"
"github.com/ZacharyZhang-NY/ely-eye/ely-eye-mcp/internal/apiclient"
"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, matched by the server's own ranking"`
Modality string `json:"modality,omitempty" jsonschema:"optional exact modality filter such as text, code, image, audio, pdf_page, pdf_region, video_frame, ui_screenshot"`
Holder string `json:"holder,omitempty" jsonschema:"optional: only this holder's memories may answer"`
Scenario string `json:"scenario,omitempty" jsonschema:"optional: only this scenario, e.g. home or work"`
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) {
result, err := s.api.Search(ctx, apiclient.SearchOptions{
Query: input.Query,
Modality: input.Modality,
Holder: input.Holder,
Scenario: input.Scenario,
Limit: input.Limit,
})
if err != nil {
return nil, SearchAtomsOutput{}, err
}
return nil, SearchAtomsOutput{Hits: contextpack.SummarizeHits(result.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.api.Search(ctx, apiclient.SearchOptions{Query: input.Question, Limit: input.Limit})
if err != nil {
return nil, CompileContextOutput{}, err
}
pack := contextpack.Build(input.Question, input.Profile, hits.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
}