Skip to main content
Glama

Paprika 3 MCP Server

by soggycactus
server.go9.28 kB
package mcpserver import ( "context" "errors" "fmt" "log/slog" "time" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "github.com/soggycactus/paprika-3-mcp/internal/paprika" ) type NewServerOptions struct { Version string Username string Password string Paprika *paprika.Client Logger *slog.Logger } func NewServer(opts NewServerOptions) (*Server, error) { paprika3, err := paprika.NewClient(opts.Username, opts.Password, opts.Version, opts.Logger) if err != nil { return nil, err } s := server.NewMCPServer("paprika-3-mcp", opts.Version, server.WithResourceCapabilities(false, false)) return &Server{ paprika3: paprika3, server: s, logger: opts.Logger, }, nil } type Server struct { paprika3 *paprika.Client logger *slog.Logger server *server.MCPServer } func (s *Server) Start() { go s.updateResources() createRecipeTool := mcp.NewTool("create_paprika_recipe", mcp.WithDescription("Save new recipes generated by LLMs in the Paprika 3 app"), mcp.WithString("name", mcp.Description("The name of the recipe"), mcp.Required()), mcp.WithString("ingredients", mcp.Description("The ingredients of the recipe"), mcp.Required()), mcp.WithString("directions", mcp.Description("The directions for the recipe"), mcp.Required()), mcp.WithString("description", mcp.Description("The description of the recipe"), mcp.DefaultString("")), mcp.WithString("notes", mcp.Description("The notes for the recipe"), mcp.DefaultString("")), mcp.WithString("servings", mcp.Description("The number of servings for the recipe"), mcp.DefaultString("")), mcp.WithString("prep_time", mcp.Description("The prep time for the recipe"), mcp.DefaultString("")), mcp.WithString("cook_time", mcp.Description("The cook time for the recipe"), mcp.DefaultString("")), mcp.WithString("difficulty", mcp.Description("The difficulty of the recipe"), mcp.DefaultString("")), ) updateRecipeTool := mcp.NewTool("update_paprika_recipe", mcp.WithDescription("Update existing recipes in the Paprika 3 app"), mcp.WithString("uid", mcp.Description("The UID of the recipe"), mcp.Required()), mcp.WithString("name", mcp.Description("The name of the recipe"), mcp.Required()), mcp.WithString("ingredients", mcp.Description("The ingredients of the recipe"), mcp.Required()), mcp.WithString("directions", mcp.Description("The directions for the recipe"), mcp.Required()), mcp.WithString("description", mcp.Description("The description of the recipe"), mcp.Required()), mcp.WithString("notes", mcp.Description("The notes for the recipe"), mcp.Required()), mcp.WithString("servings", mcp.Description("The number of servings for the recipe"), mcp.Required()), mcp.WithString("prep_time", mcp.Description("The prep time for the recipe"), mcp.Required()), mcp.WithString("cook_time", mcp.Description("The cook time for the recipe"), mcp.Required()), mcp.WithString("difficulty", mcp.Description("The difficulty of the recipe"), mcp.Required()), ) s.server.AddTools(server.ServerTool{ Tool: createRecipeTool, Handler: s.createRecipe, }, server.ServerTool{ Tool: updateRecipeTool, Handler: s.updateRecipe, }) if err := server.ServeStdio(s.server); err != nil { s.logger.Error("Server error", "err", err) } } func (s *Server) updateResources() { s.addResources() ticker := time.NewTicker(1 * time.Minute) for range ticker.C { s.addResources() } } func (s *Server) addResources() { s.logger.Info("Updating recipe resources") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // List all recipes to expose them as resources recipes, err := s.paprika3.ListRecipes(ctx) if err != nil { s.logger.Error("failed to list paprika recipes", "err", err) return } if len(recipes.Result) >= 10 { s.logger.Info("adding recipes resources concurrently") s.addResourcesConcurrently(recipes) return } for _, r := range recipes.Result { if err := s.addRecipeResource(r.UID); err != nil { s.logger.Error("failed to add recipe as MCP resource", "err", err) } } } func (s *Server) addRecipeResource(uid string) error { start := time.Now() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() recipe, err := s.paprika3.GetRecipe(ctx, uid) if err != nil { return err } if recipe.InTrash { return nil } resourceContents := mcp.TextResourceContents{ URI: fmt.Sprintf("paprika://recipes/%s", recipe.UID), MIMEType: "text/markdown", Text: recipe.ToMarkdown(), } s.server.AddResource(mcp.NewResource(fmt.Sprintf("paprika://recipes/%s", recipe.UID), recipe.Name, mcp.WithResourceDescription(recipe.ResourceDescription()), mcp.WithMIMEType("text/markdown")), func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { return []mcp.ResourceContents{resourceContents}, nil }) duration := time.Since(start) s.logger.Info("Added resource", "name", recipe.Name, "uid", recipe.UID, "duration", duration) return nil } func (s *Server) addResourcesConcurrently(recipes *paprika.RecipeList) { buffer := make(chan struct{}, 10) for _, r := range recipes.Result { go func() (err error) { buffer <- struct{}{} defer func() { <-buffer }() defer func(err error) { if err != nil { s.logger.Error("failed to add recipe as MCP resource", "err", err) } }(err) return s.addRecipeResource(r.UID) }() } } func (s *Server) createRecipe(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { start := time.Now() name, ok := req.Params.Arguments["name"].(string) if !ok || len(name) == 0 { return nil, errors.New("name is required") } ingredients, ok := req.Params.Arguments["ingredients"].(string) if !ok || len(ingredients) == 0 { return nil, errors.New("ingredients are required") } directions, ok := req.Params.Arguments["directions"].(string) if !ok || len(directions) == 0 { return nil, errors.New("directions are required") } servings := req.Params.Arguments["servings"].(string) prepTime := req.Params.Arguments["prep_time"].(string) cookTime := req.Params.Arguments["cook_time"].(string) description := req.Params.Arguments["description"].(string) notes := req.Params.Arguments["notes"].(string) difficulty := req.Params.Arguments["difficulty"].(string) ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() recipe, err := s.paprika3.SaveRecipe(ctx, paprika.Recipe{ Name: name, Ingredients: ingredients, Directions: directions, Description: description, Servings: servings, PrepTime: prepTime, CookTime: cookTime, Notes: notes, Difficulty: difficulty, }) if err != nil { return nil, err } duration := time.Since(start) s.logger.Info("Created recipe", "name", recipe.Name, "uid", recipe.UID, "duration", duration) return mcp.NewToolResultResource(recipe.Name, mcp.TextResourceContents{ URI: fmt.Sprintf("paprika://recipes/%s", recipe.UID), MIMEType: "text/markdown", Text: recipe.ToMarkdown(), }), nil } func (s *Server) updateRecipe(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { start := time.Now() uid, ok := req.Params.Arguments["uid"].(string) if !ok || len(uid) == 0 { return nil, errors.New("uid is required") } name, ok := req.Params.Arguments["name"].(string) if !ok || len(name) == 0 { return nil, errors.New("name is required") } ingredients, ok := req.Params.Arguments["ingredients"].(string) if !ok || len(ingredients) == 0 { return nil, errors.New("ingredients are required") } directions, ok := req.Params.Arguments["directions"].(string) if !ok || len(directions) == 0 { return nil, errors.New("directions are required") } description, ok := req.Params.Arguments["description"].(string) if !ok { return nil, errors.New("description is required") } servings, ok := req.Params.Arguments["servings"].(string) if !ok { return nil, errors.New("servings is required") } prepTime, ok := req.Params.Arguments["prep_time"].(string) if !ok { return nil, errors.New("prepTime is required") } cookTime, ok := req.Params.Arguments["cook_time"].(string) if !ok { return nil, errors.New("cookTime is required") } notes, ok := req.Params.Arguments["notes"].(string) if !ok { return nil, errors.New("notes is required") } difficulty, ok := req.Params.Arguments["difficulty"].(string) if !ok { return nil, errors.New("difficulty is required") } ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() recipe, err := s.paprika3.SaveRecipe(ctx, paprika.Recipe{ UID: uid, Name: name, Ingredients: ingredients, Directions: directions, Description: description, Servings: servings, PrepTime: prepTime, CookTime: cookTime, Notes: notes, Difficulty: difficulty, }) if err != nil { return nil, err } duration := time.Since(start) s.logger.Info("Updated recipe", "name", recipe.Name, "uid", recipe.UID, "duration", duration) return mcp.NewToolResultResource(recipe.Name, mcp.TextResourceContents{ URI: fmt.Sprintf("paprika://recipes/%s", recipe.UID), MIMEType: "text/markdown", Text: recipe.ToMarkdown(), }), nil }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/soggycactus/paprika-3-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server