package tools import ( "context" "fmt" "strings" "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" thoughttypes "git.warky.dev/wdevs/amcs/internal/types" ) type ContextTool struct { store *store.DB provider ai.Provider search config.SearchConfig sessions *session.ActiveProjects } type ProjectContextInput struct { Project string `json:"project,omitempty" jsonschema:"project name or id; falls back to the active session project"` Query string `json:"query,omitempty" jsonschema:"optional semantic focus for project context"` Limit int `json:"limit,omitempty" jsonschema:"maximum number of context items to return"` } type ContextItem struct { ID string `json:"id"` Content string `json:"content"` Metadata thoughttypes.ThoughtMetadata `json:"metadata"` Similarity float64 `json:"similarity,omitempty"` Source string `json:"source"` } type ProjectContextOutput struct { Project thoughttypes.Project `json:"project"` Context string `json:"context"` Items []ContextItem `json:"items"` } func NewContextTool(db *store.DB, provider ai.Provider, search config.SearchConfig, sessions *session.ActiveProjects) *ContextTool { return &ContextTool{store: db, provider: provider, search: search, sessions: sessions} } func (t *ContextTool) Handle(ctx context.Context, req *mcp.CallToolRequest, in ProjectContextInput) (*mcp.CallToolResult, ProjectContextOutput, error) { project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, true) if err != nil { return nil, ProjectContextOutput{}, err } limit := normalizeLimit(in.Limit, t.search) recent, err := t.store.RecentThoughts(ctx, &project.ID, limit, 0) if err != nil { return nil, ProjectContextOutput{}, err } items := make([]ContextItem, 0, limit*2) seen := map[string]struct{}{} for _, thought := range recent { key := thought.ID.String() seen[key] = struct{}{} items = append(items, ContextItem{ ID: key, Content: thought.Content, Metadata: thought.Metadata, Source: "recent", }) } query := strings.TrimSpace(in.Query) if query != "" { embedding, err := t.provider.Embed(ctx, query) if err != nil { return nil, ProjectContextOutput{}, err } semantic, err := t.store.SearchSimilarThoughts(ctx, embedding, t.provider.EmbeddingModel(), t.search.DefaultThreshold, limit, &project.ID, nil) if err != nil { return nil, ProjectContextOutput{}, err } for _, result := range semantic { key := result.ID.String() if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} items = append(items, ContextItem{ ID: key, Content: result.Content, Metadata: result.Metadata, Similarity: result.Similarity, Source: "semantic", }) } } lines := make([]string, 0, len(items)) for i, item := range items { lines = append(lines, thoughtContextLine(i, item.Content, item.Metadata, item.Similarity)) } contextBlock := formatContextBlock(fmt.Sprintf("Project context for %s", project.Name), lines) _ = t.store.TouchProject(ctx, project.ID) return nil, ProjectContextOutput{ Project: *project, Context: contextBlock, Items: items, }, nil }