package tools import ( "context" "log/slog" "strings" "github.com/modelcontextprotocol/go-sdk/mcp" "golang.org/x/sync/errgroup" "git.warky.dev/wdevs/amcs/internal/ai" "git.warky.dev/wdevs/amcs/internal/config" "git.warky.dev/wdevs/amcs/internal/metadata" "git.warky.dev/wdevs/amcs/internal/session" "git.warky.dev/wdevs/amcs/internal/store" thoughttypes "git.warky.dev/wdevs/amcs/internal/types" ) type CaptureTool struct { store *store.DB provider ai.Provider capture config.CaptureConfig sessions *session.ActiveProjects log *slog.Logger } type CaptureInput struct { Content string `json:"content" jsonschema:"the thought or note to capture"` Project string `json:"project,omitempty" jsonschema:"optional project name or id to associate with the thought"` } type CaptureOutput struct { Thought thoughttypes.Thought `json:"thought"` } func NewCaptureTool(db *store.DB, provider ai.Provider, capture config.CaptureConfig, sessions *session.ActiveProjects, log *slog.Logger) *CaptureTool { return &CaptureTool{store: db, provider: provider, capture: capture, sessions: sessions, log: log} } func (t *CaptureTool) Handle(ctx context.Context, req *mcp.CallToolRequest, in CaptureInput) (*mcp.CallToolResult, CaptureOutput, error) { content := strings.TrimSpace(in.Content) if content == "" { return nil, CaptureOutput{}, errInvalidInput("content is required") } project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, false) if err != nil { return nil, CaptureOutput{}, err } var embedding []float32 rawMetadata := metadata.Fallback(t.capture) group, groupCtx := errgroup.WithContext(ctx) group.Go(func() error { vector, err := t.provider.Embed(groupCtx, content) if err != nil { return err } embedding = vector return nil }) group.Go(func() error { extracted, err := t.provider.ExtractMetadata(groupCtx, content) if err != nil { t.log.Warn("metadata extraction failed, using fallback", slog.String("provider", t.provider.Name()), slog.String("error", err.Error())) return nil } rawMetadata = extracted return nil }) if err := group.Wait(); err != nil { return nil, CaptureOutput{}, err } thought := thoughttypes.Thought{ Content: content, Embedding: embedding, Metadata: metadata.Normalize(rawMetadata, t.capture), } if project != nil { thought.ProjectID = &project.ID } created, err := t.store.InsertThought(ctx, thought, t.provider.EmbeddingModel()) if err != nil { return nil, CaptureOutput{}, err } if project != nil { _ = t.store.TouchProject(ctx, project.ID) } return nil, CaptureOutput{Thought: created}, nil }