From a65d39cea0d6ace719fbcf936e0053590058b353 Mon Sep 17 00:00:00 2001 From: didier amyot Date: Tue, 16 Jun 2026 13:39:41 -0400 Subject: [PATCH] prepared last tool --- main.go | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ sqlite.go | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/main.go b/main.go index f48217c..6386ae9 100644 --- a/main.go +++ b/main.go @@ -58,6 +58,7 @@ func main() { registerStoreFlashcard(mcpServer, store, searchEngine, logger) registerSearchFlashcard(mcpServer, searchEngine) + registerListRecentFlashcard(mcpServer, store, logger) registerUpdateFlashcard(mcpServer, store, searchEngine, logger) registerDeleteFlashcard(mcpServer, store, searchEngine, logger) registerVoteFlashcard(mcpServer, store, logger) @@ -245,6 +246,65 @@ func registerSearchFlashcard(mcpServer *server.MCPServer, engine SearchEngine) { ) } +func registerListRecentFlashcard(mcpServer *server.MCPServer, sqlite *sqliteStore, logger *zap.Logger) { + mcpServer.AddTool( + mcp.NewTool("list_recent_flashcards", + mcp.WithDescription("List recently created flashcards, optionally filtered by tag, repo, or host. Use this at the start of a session to recall what was learned recently."), + mcp.WithNumber("limit", + mcp.Description("Maximum number of results to return (default: 20)"), + ), + mcp.WithString("tag", + mcp.Description("Filter by tag (e.g. 'preference', 'convention')"), + ), + mcp.WithString("repo", + mcp.Description("Filter by repository name (e.g. 'st-pierre', 'loopagent')"), + ), + mcp.WithString("host", + mcp.Description("Filter by hostname (e.g. 'shulgi')"), + ), + ), + handleListRecentFlashcard(sqlite, logger), + ) +} + +func handleListRecentFlashcard(sqlite *sqliteStore, logger *zap.Logger) server.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + args, ok := request.Params.Arguments.(map[string]any) + if !ok { + return mcp.NewToolResultError("invalid arguments"), nil + } + + tag := parseOptionalString(args["tag"]) + repo := parseOptionalString(args["repo"]) + host := parseOptionalString(args["host"]) + + limit := uint64(20) + if rawLimit := args["limit"]; rawLimit != nil { + switch v := rawLimit.(type) { + case float64: + limit = uint64(v) + case int: + limit = uint64(v) + case int64: + limit = uint64(v) + } + } + + entries, err := sqlite.listRecent(ctx, tag, repo, host, limit) + if err != nil { + logger.Error("list recent failed", zap.Error(err)) + return mcp.NewToolResultError(fmt.Sprintf("list recent failed: %v", err)), nil + } + + output, err := json.MarshalIndent(entries, "", " ") + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal results: %v", err)), nil + } + + return mcp.NewToolResultText(string(output)), nil + } +} + func registerUpdateFlashcard(mcpServer *server.MCPServer, sqlite *sqliteStore, engine SearchEngine, logger *zap.Logger) { mcpServer.AddTool( mcp.NewTool("update_flashcard", diff --git a/sqlite.go b/sqlite.go index aef2add..775babc 100644 --- a/sqlite.go +++ b/sqlite.go @@ -364,6 +364,62 @@ func (s *sqliteStore) searchFTS(ctx context.Context, query string, limit uint64) return results, nil } +func (s *sqliteStore) listRecent(ctx context.Context, tag, repo, host string, limit uint64) ([]flashcardSummary, error) { + if limit == 0 { + limit = 20 + } + + q := `SELECT f.public_id, f.version, f.note, f.host, f.user, f.tags, f.repos, f.created + FROM flashcard f + WHERE f.version = (SELECT MAX(f2.version) FROM flashcard f2 WHERE f2.public_id = f.public_id) + AND f.delete_timestamp IS NULL + AND f.public_id != 0` + var args []any + + if tag != "" { + q += ` AND f.tags LIKE ?` + args = append(args, `%"`+tag+`"%`) + } + if repo != "" { + q += ` AND f.repos LIKE ?` + args = append(args, `%"`+repo+`"%`) + } + if host != "" { + q += ` AND f.host = ?` + args = append(args, host) + } + + q += ` ORDER BY f.created DESC LIMIT ?` + args = append(args, limit) + + rows, err := s.db.QueryContext(ctx, q, args...) + if err != nil { + return nil, fmt.Errorf("list recent: %w", err) + } + defer rows.Close() + + var summaries []flashcardSummary + for rows.Next() { + var s flashcardSummary + var tagsJSON, reposJSON *string + if err := rows.Scan(&s.PublicID, &s.Version, &s.Note, &s.Host, &s.User, &tagsJSON, &reposJSON, &s.Created); err != nil { + return nil, fmt.Errorf("scan: %w", err) + } + if tagsJSON != nil { + json.Unmarshal([]byte(*tagsJSON), &s.Tags) + } + if reposJSON != nil { + json.Unmarshal([]byte(*reposJSON), &s.Repos) + } + s.URL = fmt.Sprintf("/api/v1/flashcards/%d", s.PublicID) + summaries = append(summaries, s) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("rows: %w", err) + } + return summaries, nil +} + func (s *sqliteStore) close() error { return s.db.Close() }