package tools import ( "context" "fmt" "strings" "github.com/google/uuid" "github.com/modelcontextprotocol/go-sdk/mcp" "git.warky.dev/wdevs/amcs/internal/ai" "git.warky.dev/wdevs/amcs/internal/config" "git.warky.dev/wdevs/amcs/internal/session" "git.warky.dev/wdevs/amcs/internal/store" ) type RecallTool struct { store *store.DB provider ai.Provider search config.SearchConfig sessions *session.ActiveProjects } type RecallInput struct { Query string `json:"query" jsonschema:"semantic query for recalled context"` Project string `json:"project,omitempty" jsonschema:"optional project name or id; falls back to the active session project"` Limit int `json:"limit,omitempty" jsonschema:"maximum number of context items to return"` } type RecallOutput struct { Context string `json:"context"` Items []ContextItem `json:"items"` } func NewRecallTool(db *store.DB, provider ai.Provider, search config.SearchConfig, sessions *session.ActiveProjects) *RecallTool { return &RecallTool{store: db, provider: provider, search: search, sessions: sessions} } func (t *RecallTool) Handle(ctx context.Context, req *mcp.CallToolRequest, in RecallInput) (*mcp.CallToolResult, RecallOutput, error) { query := strings.TrimSpace(in.Query) if query == "" { return nil, RecallOutput{}, errInvalidInput("query is required") } project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, false) if err != nil { return nil, RecallOutput{}, err } limit := normalizeLimit(in.Limit, t.search) embedding, err := t.provider.Embed(ctx, query) if err != nil { return nil, RecallOutput{}, err } var projectID *uuid.UUID if project != nil { projectID = &project.ID } semantic, err := t.store.SearchSimilarThoughts(ctx, embedding, t.search.DefaultThreshold, limit, projectID, nil) if err != nil { return nil, RecallOutput{}, err } recent, err := t.store.RecentThoughts(ctx, projectID, limit, 0) if err != nil { return nil, RecallOutput{}, err } items := make([]ContextItem, 0, limit*2) seen := map[string]struct{}{} for _, result := range semantic { key := result.ID.String() seen[key] = struct{}{} items = append(items, ContextItem{ ID: key, Content: result.Content, Metadata: result.Metadata, Similarity: result.Similarity, Source: "semantic", }) } for _, thought := range recent { key := thought.ID.String() if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} items = append(items, ContextItem{ ID: key, Content: thought.Content, Metadata: thought.Metadata, Source: "recent", }) } lines := make([]string, 0, len(items)) for i, item := range items { lines = append(lines, thoughtContextLine(i, item.Content, item.Metadata, item.Similarity)) } header := "Recalled context" if project != nil { header = fmt.Sprintf("Recalled context for %s", project.Name) _ = t.store.TouchProject(ctx, project.ID) } return nil, RecallOutput{ Context: formatContextBlock(header, lines), Items: items, }, nil }