feat: added first class citizen tags
This commit is contained in:
1 parent
57bcd549d1
commit
4dc42d91c2
4 files changed
+469
-38
No files matched your search
@@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -59,6 +60,10 @@ func main() {
|
||||
registerStoreFlashcard(mcpServer, store, searchEngine, logger)
|
||||
registerSearchFlashcard(mcpServer, searchEngine)
|
||||
registerListRecentFlashcard(mcpServer, store, logger)
|
||||
registerListTags(mcpServer, store, logger)
|
||||
registerCreateTag(mcpServer, store, logger)
|
||||
registerUpdateTag(mcpServer, store, logger)
|
||||
registerMergeTag(mcpServer, store, logger)
|
||||
registerUpdateFlashcard(mcpServer, store, searchEngine, logger)
|
||||
registerDeleteFlashcard(mcpServer, store, searchEngine, logger)
|
||||
registerVoteFlashcard(mcpServer, store, logger)
|
||||
@@ -204,7 +209,7 @@ func createSearchEngine(logger *zap.Logger, sqliteStore *sqliteStore, openAIKey
|
||||
func registerStoreFlashcard(mcpServer *server.MCPServer, sqlite *sqliteStore, engine SearchEngine, logger *zap.Logger) {
|
||||
mcpServer.AddTool(
|
||||
mcp.NewTool("store_flashcard",
|
||||
mcp.WithDescription("Store a flashcard for later retrieval. Use this to persist useful information discovered during a session."),
|
||||
mcp.WithDescription("Store a flashcard for later retrieval. Use this to persist useful information discovered during a session. Use as few tags as possible — ideally 0 to 2 — and reuse existing tags (see list_tags) rather than inventing new ones; create a genuinely new tag with create_tag first."),
|
||||
mcp.WithString("note",
|
||||
mcp.Required(),
|
||||
mcp.Description("The markdown body — keep it concise, one paragraph usually"),
|
||||
@@ -269,6 +274,184 @@ func registerListRecentFlashcard(mcpServer *server.MCPServer, sqlite *sqliteStor
|
||||
)
|
||||
}
|
||||
|
||||
func registerListTags(mcpServer *server.MCPServer, sqlite *sqliteStore, logger *zap.Logger) {
|
||||
mcpServer.AddTool(
|
||||
mcp.NewTool("list_tags",
|
||||
mcp.WithDescription("List the tag vocabulary — every known tag with its description and how many flashcards use it. Use this before storing a flashcard to reuse existing tags instead of inventing new ones."),
|
||||
),
|
||||
handleListTags(sqlite, logger),
|
||||
)
|
||||
}
|
||||
|
||||
func handleListTags(sqlite *sqliteStore, logger *zap.Logger) server.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
tags, err := sqlite.listTags(ctx)
|
||||
if err != nil {
|
||||
logger.Error("list tags failed", zap.Error(err))
|
||||
return mcp.NewToolResultError(fmt.Sprintf("list tags failed: %v", err)), nil
|
||||
}
|
||||
|
||||
output, err := json.MarshalIndent(tags, "", " ")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to marshal results: %v", err)), nil
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(string(output)), nil
|
||||
}
|
||||
}
|
||||
|
||||
func registerCreateTag(mcpServer *server.MCPServer, sqlite *sqliteStore, logger *zap.Logger) {
|
||||
mcpServer.AddTool(
|
||||
mcp.NewTool("create_tag",
|
||||
mcp.WithDescription("Create a tag in the vocabulary. Tags are shared across flashcards. Check list_tags first and reuse an existing tag when one fits; create a new one only when nothing does. Fails if the tag already exists."),
|
||||
mcp.WithString("name",
|
||||
mcp.Required(),
|
||||
mcp.Description("Tag name — short, lowercase, no spaces (e.g. 'preference', 'workaround')"),
|
||||
),
|
||||
mcp.WithString("description",
|
||||
mcp.Description("What the tag means and when to use it"),
|
||||
),
|
||||
),
|
||||
handleCreateTag(sqlite, logger),
|
||||
)
|
||||
}
|
||||
|
||||
func handleCreateTag(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
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(parseOptionalString(args["name"]))
|
||||
if name == "" {
|
||||
return mcp.NewToolResultError("name is required and must be a non-empty string"), nil
|
||||
}
|
||||
|
||||
tag, err := sqlite.createTag(ctx, name, parseOptionalString(args["description"]))
|
||||
if err != nil {
|
||||
logger.Error("create tag failed", zap.String("name", name), zap.Error(err))
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to create tag: %v", err)), nil
|
||||
}
|
||||
|
||||
output, err := json.MarshalIndent(tag, "", " ")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(string(output)), nil
|
||||
}
|
||||
}
|
||||
|
||||
func registerUpdateTag(mcpServer *server.MCPServer, sqlite *sqliteStore, logger *zap.Logger) {
|
||||
mcpServer.AddTool(
|
||||
mcp.NewTool("update_tag",
|
||||
mcp.WithDescription("Update a tag's name and/or description. Renaming keeps the tag linked to its flashcards. Fails if the new name is already used by another tag."),
|
||||
mcp.WithString("name",
|
||||
mcp.Required(),
|
||||
mcp.Description("Current tag name"),
|
||||
),
|
||||
mcp.WithString("new_name",
|
||||
mcp.Description("New name, if renaming"),
|
||||
),
|
||||
mcp.WithString("description",
|
||||
mcp.Description("New description; pass an empty string to clear it"),
|
||||
),
|
||||
),
|
||||
handleUpdateTag(sqlite, logger),
|
||||
)
|
||||
}
|
||||
|
||||
func handleUpdateTag(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
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(parseOptionalString(args["name"]))
|
||||
if name == "" {
|
||||
return mcp.NewToolResultError("name is required and must be a non-empty string"), nil
|
||||
}
|
||||
|
||||
newName := strings.TrimSpace(parseOptionalString(args["new_name"]))
|
||||
description, setDescription := "", false
|
||||
if raw, present := args["description"]; present && raw != nil {
|
||||
description, setDescription = parseOptionalString(raw), true
|
||||
}
|
||||
|
||||
if newName == "" && !setDescription {
|
||||
return mcp.NewToolResultError("nothing to update: provide new_name and/or description"), nil
|
||||
}
|
||||
|
||||
tag, err := sqlite.updateTag(ctx, name, newName, description, setDescription)
|
||||
if err != nil {
|
||||
logger.Error("update tag failed", zap.String("name", name), zap.Error(err))
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to update tag: %v", err)), nil
|
||||
}
|
||||
|
||||
output, err := json.MarshalIndent(tag, "", " ")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(string(output)), nil
|
||||
}
|
||||
}
|
||||
|
||||
func registerMergeTag(mcpServer *server.MCPServer, sqlite *sqliteStore, logger *zap.Logger) {
|
||||
mcpServer.AddTool(
|
||||
mcp.NewTool("merge_tag",
|
||||
mcp.WithDescription("Merge tag 'from' into tag 'into': every flashcard using 'from' is repointed to 'into', then 'from' is deleted. Use this to collapse near-duplicate tags (e.g. merge 'golang' into 'go'). Optionally set the merged tag's description."),
|
||||
mcp.WithString("from",
|
||||
mcp.Required(),
|
||||
mcp.Description("Tag to merge away and delete"),
|
||||
),
|
||||
mcp.WithString("into",
|
||||
mcp.Required(),
|
||||
mcp.Description("Tag that survives and absorbs the flashcards"),
|
||||
),
|
||||
mcp.WithString("description",
|
||||
mcp.Description("Description to set on the surviving tag; omitted keeps its current description"),
|
||||
),
|
||||
),
|
||||
handleMergeTag(sqlite, logger),
|
||||
)
|
||||
}
|
||||
|
||||
func handleMergeTag(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
|
||||
}
|
||||
|
||||
from := strings.TrimSpace(parseOptionalString(args["from"]))
|
||||
into := strings.TrimSpace(parseOptionalString(args["into"]))
|
||||
if from == "" || into == "" {
|
||||
return mcp.NewToolResultError("from and into are required and must be non-empty strings"), nil
|
||||
}
|
||||
|
||||
description, setDescription := "", false
|
||||
if raw, present := args["description"]; present && raw != nil {
|
||||
description, setDescription = parseOptionalString(raw), true
|
||||
}
|
||||
|
||||
tag, err := sqlite.mergeTag(ctx, from, into, description, setDescription)
|
||||
if err != nil {
|
||||
logger.Error("merge tag failed", zap.String("from", from), zap.String("into", into), zap.Error(err))
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to merge tag: %v", err)), nil
|
||||
}
|
||||
|
||||
output, err := json.MarshalIndent(tag, "", " ")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(string(output)), nil
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -385,7 +568,7 @@ func handleUpdateFlashcard(sqlite *sqliteStore, engine SearchEngine, logger *zap
|
||||
}
|
||||
|
||||
version := latest.Version + 1
|
||||
id, err := sqlite.save(ctx, note, host, user, context_, repos, tags, publicID)
|
||||
id, err := sqlite.save(ctx, note, host, user, context_, repos, tags, publicID, version)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to persist update: %v", err)), nil
|
||||
}
|
||||
@@ -564,7 +747,7 @@ func handleStoreFlashcard(sqlite *sqliteStore, engine SearchEngine, logger *zap.
|
||||
context_ := parseOptionalString(args["context"])
|
||||
publicID := parseInt64(args["public_id"])
|
||||
|
||||
id, err := sqlite.save(ctx, note, host, user, context_, repos, tags, publicID)
|
||||
id, err := sqlite.save(ctx, note, host, user, context_, repos, tags, publicID, 1)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to persist: %v", err)), nil
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user