mirror of https://github.com/usememos/memos
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
181 lines
5.9 KiB
Go
181 lines
5.9 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/mark3labs/mcp-go/mcp"
|
|
mcpserver "github.com/mark3labs/mcp-go/server"
|
|
|
|
v1pb "github.com/usememos/memos/proto/gen/api/v1"
|
|
"github.com/usememos/memos/server/auth"
|
|
"github.com/usememos/memos/store"
|
|
)
|
|
|
|
type reactionJSON struct {
|
|
ID int32 `json:"id"`
|
|
Creator string `json:"creator"`
|
|
ReactionType string `json:"reaction_type"`
|
|
CreateTime int64 `json:"create_time"`
|
|
}
|
|
|
|
func (s *MCPService) registerReactionTools(mcpSrv *mcpserver.MCPServer) {
|
|
mcpSrv.AddTool(mcp.NewTool("list_reactions",
|
|
readOnlyToolOptions("List reactions", "List all reactions on a memo. Returns reaction type and creator for each reaction.",
|
|
mcp.WithString("name", mcp.Required(), mcp.Description(`Memo resource name, e.g. "memos/abc123"`)),
|
|
)...,
|
|
), s.handleListReactions)
|
|
|
|
mcpSrv.AddTool(mcp.NewTool("upsert_reaction",
|
|
createToolOptions("Upsert reaction", "Add a reaction (emoji) to a memo. If the same reaction already exists from the same user, this is a no-op. Requires authentication.", true,
|
|
mcp.WithString("name", mcp.Required(), mcp.Description(`Memo resource name, e.g. "memos/abc123"`)),
|
|
mcp.WithString("reaction_type", mcp.Required(), mcp.Description(`Reaction emoji, e.g. "👍", "❤️", "🎉"`)),
|
|
mcp.WithOutputSchema[reactionJSON](),
|
|
)...,
|
|
), s.handleUpsertReaction)
|
|
|
|
mcpSrv.AddTool(mcp.NewTool("delete_reaction",
|
|
updateToolOptions("Delete reaction", "Remove a reaction by its ID. Requires authentication and ownership of the reaction.",
|
|
mcp.WithNumber("id", mcp.Required(), mcp.Description("Reaction ID to delete")),
|
|
mcp.WithOutputSchema[deletedJSON](),
|
|
)...,
|
|
), s.handleDeleteReaction)
|
|
}
|
|
|
|
func (s *MCPService) handleListReactions(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
userID := auth.GetUserID(ctx)
|
|
|
|
uid, err := parseMemoUID(req.GetString("name", ""))
|
|
if err != nil {
|
|
return mcp.NewToolResultError(err.Error()), nil
|
|
}
|
|
|
|
memo, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &uid})
|
|
if err != nil {
|
|
return mcp.NewToolResultError(fmt.Sprintf("failed to get memo: %v", err)), nil
|
|
}
|
|
if memo == nil {
|
|
return mcp.NewToolResultError("memo not found"), nil
|
|
}
|
|
if err := checkMemoAccess(memo, userID); err != nil {
|
|
return mcp.NewToolResultError(err.Error()), nil
|
|
}
|
|
|
|
contentID := "memos/" + uid
|
|
reactions, err := s.store.ListReactions(ctx, &store.FindReaction{ContentID: &contentID})
|
|
if err != nil {
|
|
return mcp.NewToolResultError(fmt.Sprintf("failed to list reactions: %v", err)), nil
|
|
}
|
|
creatorIDs := make([]int32, 0, len(reactions))
|
|
for _, reaction := range reactions {
|
|
creatorIDs = append(creatorIDs, reaction.CreatorID)
|
|
}
|
|
usernamesByID, err := preloadUsernames(ctx, s.store, creatorIDs)
|
|
if err != nil {
|
|
return mcp.NewToolResultError(fmt.Sprintf("failed to preload reaction creators: %v", err)), nil
|
|
}
|
|
|
|
results := make([]reactionJSON, len(reactions))
|
|
for i, r := range reactions {
|
|
creator, err := lookupUsernameFromCache(usernamesByID, r.CreatorID)
|
|
if err != nil {
|
|
return mcp.NewToolResultError(fmt.Sprintf("failed to resolve reaction creator: %v", err)), nil
|
|
}
|
|
results[i] = reactionJSON{
|
|
ID: r.ID,
|
|
Creator: creator,
|
|
ReactionType: r.ReactionType,
|
|
CreateTime: r.CreatedTs,
|
|
}
|
|
}
|
|
|
|
return newToolResultJSON(results)
|
|
}
|
|
|
|
func (s *MCPService) handleUpsertReaction(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
userID, err := extractUserID(ctx)
|
|
if err != nil {
|
|
return mcp.NewToolResultError(err.Error()), nil
|
|
}
|
|
|
|
uid, err := parseMemoUID(req.GetString("name", ""))
|
|
if err != nil {
|
|
return mcp.NewToolResultError(err.Error()), nil
|
|
}
|
|
reactionType := req.GetString("reaction_type", "")
|
|
if reactionType == "" {
|
|
return mcp.NewToolResultError("reaction_type is required"), nil
|
|
}
|
|
|
|
memo, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &uid})
|
|
if err != nil {
|
|
return mcp.NewToolResultError(fmt.Sprintf("failed to get memo: %v", err)), nil
|
|
}
|
|
if memo == nil {
|
|
return mcp.NewToolResultError("memo not found"), nil
|
|
}
|
|
if err := checkMemoAccess(memo, userID); err != nil {
|
|
return mcp.NewToolResultError(err.Error()), nil
|
|
}
|
|
|
|
// Validate reaction type against allowed reactions.
|
|
memoRelatedSetting, err := s.store.GetInstanceMemoRelatedSetting(ctx)
|
|
if err != nil {
|
|
return mcp.NewToolResultError(fmt.Sprintf("failed to get reaction settings: %v", err)), nil
|
|
}
|
|
allowed := false
|
|
for _, r := range memoRelatedSetting.Reactions {
|
|
if r == reactionType {
|
|
allowed = true
|
|
break
|
|
}
|
|
}
|
|
if !allowed {
|
|
return mcp.NewToolResultError(fmt.Sprintf("reaction %q is not in the allowed reaction list", reactionType)), nil
|
|
}
|
|
|
|
contentID := "memos/" + uid
|
|
reaction, err := s.apiV1Service.UpsertMemoReaction(ctx, &v1pb.UpsertMemoReactionRequest{
|
|
Name: contentID,
|
|
Reaction: &v1pb.Reaction{
|
|
ContentId: contentID,
|
|
ReactionType: reactionType,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return mcp.NewToolResultError(fmt.Sprintf("failed to upsert reaction: %v", err)), nil
|
|
}
|
|
|
|
result, err := s.loadReactionJSONByName(ctx, reaction.Name)
|
|
if err != nil {
|
|
return mcp.NewToolResultError(err.Error()), nil
|
|
}
|
|
return newToolResultJSON(result)
|
|
}
|
|
|
|
func (s *MCPService) handleDeleteReaction(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
if _, err := extractUserID(ctx); err != nil {
|
|
return mcp.NewToolResultError(err.Error()), nil
|
|
}
|
|
|
|
reactionID := int32(req.GetInt("id", 0))
|
|
if reactionID == 0 {
|
|
return mcp.NewToolResultError("id is required"), nil
|
|
}
|
|
|
|
reaction, err := s.store.GetReaction(ctx, &store.FindReaction{ID: &reactionID})
|
|
if err != nil {
|
|
return mcp.NewToolResultError(fmt.Sprintf("failed to get reaction: %v", err)), nil
|
|
}
|
|
if reaction == nil {
|
|
return mcp.NewToolResultError("reaction not found"), nil
|
|
}
|
|
|
|
if _, err := s.apiV1Service.DeleteMemoReaction(ctx, &v1pb.DeleteMemoReactionRequest{
|
|
Name: fmt.Sprintf("%s/reactions/%d", reaction.ContentID, reactionID),
|
|
}); err != nil {
|
|
return mcp.NewToolResultError(fmt.Sprintf("failed to delete reaction: %v", err)), nil
|
|
}
|
|
return newDeletedToolResult()
|
|
}
|