feat: add vote_flashcard MCP tool
This commit is contained in:
1 parent
cd8e872491
commit
a40542f7b4
4 files changed
+143
No files matched your search
@@ -54,6 +54,7 @@ func main() {
|
||||
registerSearchFlashcard(mcpServer, searchEngine)
|
||||
registerUpdateFlashcard(mcpServer, sqliteStore, searchEngine, logger)
|
||||
registerDeleteFlashcard(mcpServer, sqliteStore, searchEngine, logger)
|
||||
registerVoteFlashcard(mcpServer, sqliteStore, logger)
|
||||
|
||||
streamableServer := server.NewStreamableHTTPServer(mcpServer)
|
||||
|
||||
@@ -334,6 +335,92 @@ func handleDeleteFlashcard(sqlite *sqliteStore, engine SearchEngine, logger *zap
|
||||
}
|
||||
}
|
||||
|
||||
func registerVoteFlashcard(mcpServer *server.MCPServer, sqlite *sqliteStore, logger *zap.Logger) {
|
||||
mcpServer.AddTool(
|
||||
mcp.NewTool("vote_flashcard",
|
||||
mcp.WithDescription("Vote on a flashcard. Rate it from -3 (worst) to 3 (best). Requires detailed context about who is voting and why."),
|
||||
mcp.WithNumber("public_id",
|
||||
mcp.Required(),
|
||||
mcp.Description("Public identifier of the flashcard to vote on"),
|
||||
),
|
||||
mcp.WithNumber("value",
|
||||
mcp.Required(),
|
||||
mcp.Description("Vote value from -3 to 3"),
|
||||
),
|
||||
mcp.WithString("user",
|
||||
mcp.Required(),
|
||||
mcp.Description("Who is voting"),
|
||||
),
|
||||
mcp.WithString("repo",
|
||||
mcp.Required(),
|
||||
mcp.Description("Repository context for the vote"),
|
||||
),
|
||||
mcp.WithString("message",
|
||||
mcp.Required(),
|
||||
mcp.Description("Justification or context for the vote"),
|
||||
),
|
||||
),
|
||||
handleVoteFlashcard(sqlite, logger),
|
||||
)
|
||||
}
|
||||
|
||||
func handleVoteFlashcard(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
|
||||
}
|
||||
|
||||
publicID := parseInt64(args["public_id"])
|
||||
if publicID == 0 {
|
||||
return mcp.NewToolResultError("public_id is required and must be a positive integer"), nil
|
||||
}
|
||||
|
||||
value := int(parseInt64(args["value"]))
|
||||
if value < -3 || value > 3 {
|
||||
return mcp.NewToolResultError("value must be between -3 and 3"), nil
|
||||
}
|
||||
|
||||
user := parseOptionalString(args["user"])
|
||||
if user == "" {
|
||||
return mcp.NewToolResultError("user is required"), nil
|
||||
}
|
||||
|
||||
repo := parseOptionalString(args["repo"])
|
||||
if repo == "" {
|
||||
return mcp.NewToolResultError("repo is required"), nil
|
||||
}
|
||||
|
||||
message := parseOptionalString(args["message"])
|
||||
if message == "" {
|
||||
return mcp.NewToolResultError("message is required"), nil
|
||||
}
|
||||
|
||||
latest, err := sqlite.getLatestByPublicID(ctx, publicID)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to query: %v", err)), nil
|
||||
}
|
||||
if latest == nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("no flashcard found with public_id %d", publicID)), nil
|
||||
}
|
||||
if latest.DeleteTimestamp != "" {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("flashcard %d is deleted", publicID)), nil
|
||||
}
|
||||
|
||||
v, err := sqlite.saveVote(ctx, latest.ID, value, user, repo, message)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to save vote: %v", err)), nil
|
||||
}
|
||||
|
||||
output, err := json.MarshalIndent(v, "", " ")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to marshal: %v", err)), nil
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(string(output)), nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleStoreFlashcard(sqlite *sqliteStore, engine SearchEngine, logger *zap.Logger) server.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args, ok := request.Params.Arguments.(map[string]any)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
-- +goose Up
|
||||
CREATE TABLE IF NOT EXISTS vote (
|
||||
_id INTEGER PRIMARY KEY,
|
||||
flashcard_id INTEGER NOT NULL REFERENCES flashcard(_id),
|
||||
value INTEGER NOT NULL CHECK(value >= -3 AND value <= 3),
|
||||
"user" TEXT NOT NULL,
|
||||
repo TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
created TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE(flashcard_id, "user", repo)
|
||||
);
|
||||
|
||||
-- +goose Down
|
||||
DROP TABLE IF EXISTS vote;
|
||||
@@ -43,6 +43,11 @@ func newSqliteStore(path string) (*sqliteStore, error) {
|
||||
return nil, fmt.Errorf("sqlite ping: %w", err)
|
||||
}
|
||||
|
||||
if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("enable foreign keys: %w", err)
|
||||
}
|
||||
|
||||
goose.SetBaseFS(embedMigrations)
|
||||
|
||||
if err := goose.SetDialect("sqlite3"); err != nil {
|
||||
@@ -233,6 +238,33 @@ func (s *sqliteStore) getByPublicIDAndVersion(ctx context.Context, publicID int6
|
||||
return &e, nil
|
||||
}
|
||||
|
||||
func (s *sqliteStore) saveVote(ctx context.Context, flashcardID int64, value int, user, repo, message string) (*vote, error) {
|
||||
_, err := s.db.ExecContext(ctx,
|
||||
`INSERT INTO vote (flashcard_id, value, "user", repo, message)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(flashcard_id, "user", repo)
|
||||
DO UPDATE SET value = excluded.value, message = excluded.message`,
|
||||
flashcardID, value, user, repo, message,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("upsert vote: %w", err)
|
||||
}
|
||||
|
||||
row := s.db.QueryRowContext(ctx,
|
||||
`SELECT _id, flashcard_id, value, "user", repo, message, created
|
||||
FROM vote WHERE flashcard_id = ? AND "user" = ? AND repo = ?`,
|
||||
flashcardID, user, repo,
|
||||
)
|
||||
|
||||
var v vote
|
||||
err = row.Scan(&v.ID, &v.FlashcardID, &v.Value, &v.User, &v.Repo, &v.Message, &v.Created)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read vote: %w", err)
|
||||
}
|
||||
|
||||
return &v, nil
|
||||
}
|
||||
|
||||
func (s *sqliteStore) tombstone(ctx context.Context, publicID int64, version int) (int64, error) {
|
||||
result, err := s.db.ExecContext(ctx,
|
||||
`INSERT INTO flashcard (public_id, version, note, delete_timestamp)
|
||||
|
||||
@@ -14,6 +14,16 @@ type entry struct {
|
||||
Created string `json:"created"`
|
||||
}
|
||||
|
||||
type vote struct {
|
||||
ID int64 `json:"_id"`
|
||||
FlashcardID int64 `json:"flashcard_id"`
|
||||
Value int `json:"value"`
|
||||
User string `json:"user"`
|
||||
Repo string `json:"repo"`
|
||||
Message string `json:"message"`
|
||||
Created string `json:"created"`
|
||||
}
|
||||
|
||||
type searchResult struct {
|
||||
Entry entry `json:",inline"`
|
||||
Score float32 `json:"score"`
|
||||
|
||||
Reference in new issue
Block a user