commit 4151e02f7450722a0aa9fe6f7552c705147aba56 Author: Tai Groot Date: Tue Jul 22 03:15:18 2025 -0700 initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..003fe2c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +main +gotify-mcp +.crush/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..ccd627d --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# Gotify MCP Server + +A Model Context Protocol (MCP) server that enables LLMs to send notifications to a Gotify server. This allows AI assistants to notify users about task completion, request help, or provide activity summaries. + +## Features + +- **Send Message**: Send custom messages with configurable priority and title +- **Ask for Help**: Send help request notifications with context and error details +- **Notify Completion**: Send task completion notifications with results +- **Summarize Activity**: Send activity summaries with optional details + +## Environment Variables + +The MCP server requires the following environment variables: + +- `GOTIFY_URL`: The URL of your Gotify server (e.g., `https://gotify.example.com`) +- `GOTIFY_TOKEN`: Your Gotify application token for authentication + +## Installation + +1. Clone the repository: +```bash +git clone https://github.com/taigrr/gotify-mcp.git +cd gotify-mcp +``` + +2. Build the binary: +```bash +go build -o gotify-mcp +``` + +3. Set up environment variables: +```bash +export GOTIFY_URL="https://your-gotify-server.com" +export GOTIFY_TOKEN="your-application-token" +``` + +## Usage + +The MCP server communicates over stdio and provides the following tools: + +### send-message +Send a custom message to Gotify. + +**Parameters:** +- `message` (required): The message content to send +- `title` (optional): Title for the message +- `priority` (optional): Message priority (0-10, default: 5) + +### ask-for-help +Send a help request notification. + +**Parameters:** +- `context` (required): Context or description of what help is needed +- `error` (optional): Optional error message or details + +### notify-completion +Send a task completion notification. + +**Parameters:** +- `task` (required): Description of the completed task +- `result` (optional): Optional result or outcome details + +### summarize-activity +Send an activity summary notification. + +**Parameters:** +- `summary` (required): Summary of activities or current status +- `details` (optional): Optional additional details + +## Integration with MCP Clients + +To use this MCP server with an MCP client, configure it to run the `gotify-mcp` binary with the appropriate environment variables set. + +Example configuration for Claude Desktop: + +```json +{ + "mcpServers": { + "gotify": { + "command": "/path/to/gotify-mcp", + "env": { + "GOTIFY_URL": "https://your-gotify-server.com", + "GOTIFY_TOKEN": "your-application-token" + } + } + } +} +``` + +## License + +This project is licensed under the MIT License. \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fefd5a1 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/taigrr/gotify-mcp + +go 1.24.5 + +require github.com/mark3labs/mcp-go v0.33.0 + +require ( + github.com/google/uuid v1.6.0 // indirect + github.com/spf13/cast v1.7.1 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6d7cb84 --- /dev/null +++ b/go.sum @@ -0,0 +1,26 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mark3labs/mcp-go v0.33.0 h1:naxhjnTIs/tyPZmWUZFuG0lDmdA6sUyYGGf3gsHvTCc= +github.com/mark3labs/mcp-go v0.33.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..75c3f71 --- /dev/null +++ b/main.go @@ -0,0 +1,247 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "os" + + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +type GotifyMessage struct { + Message string `json:"message"` + Title string `json:"title,omitempty"` + Priority int `json:"priority,omitempty"` +} + +func getStringArg(args map[string]interface{}, key string, defaultValue string) string { + if val, ok := args[key].(string); ok { + return val + } + return defaultValue +} + +func getNumberArg(args map[string]interface{}, key string, defaultValue float64) float64 { + if val, ok := args[key].(float64); ok { + return val + } + return defaultValue +} + +func main() { + s := server.NewMCPServer( + "Gotify Notification Server", + "1.0.0", + server.WithToolCapabilities(false), + ) + + sendMessageTool := mcp.NewTool("send-message", + mcp.WithDescription("Send a message to a Gotify server for notifications"), + mcp.WithString("message", + mcp.Required(), + mcp.Description("The message content to send"), + ), + mcp.WithString("title", + mcp.Description("Optional title for the message"), + ), + mcp.WithNumber("priority", + mcp.Description("Message priority (0-10, default: 5)"), + ), + ) + + askForHelpTool := mcp.NewTool("ask-for-help", + mcp.WithDescription("Send a help request notification to the user via Gotify"), + mcp.WithString("context", + mcp.Required(), + mcp.Description("Context or description of what help is needed"), + ), + mcp.WithString("error", + mcp.Description("Optional error message or details"), + ), + ) + + notifyCompletionTool := mcp.NewTool("notify-completion", + mcp.WithDescription("Send a completion notification to the user via Gotify"), + mcp.WithString("task", + mcp.Required(), + mcp.Description("Description of the completed task"), + ), + mcp.WithString("result", + mcp.Description("Optional result or outcome details"), + ), + ) + + summarizeTool := mcp.NewTool("summarize-activity", + mcp.WithDescription("Send a summary of current activities or status to the user via Gotify"), + mcp.WithString("summary", + mcp.Required(), + mcp.Description("Summary of activities or current status"), + ), + mcp.WithString("details", + mcp.Description("Optional additional details"), + ), + ) + + s.AddTool(sendMessageTool, sendMessage) + s.AddTool(askForHelpTool, askForHelp) + s.AddTool(notifyCompletionTool, notifyCompletion) + s.AddTool(summarizeTool, summarizeActivity) + + if err := server.ServeStdio(s); err != nil { + fmt.Printf("Server error: %v\n", err) + } +} + +func sendGotifyMessage(message GotifyMessage) error { + gotifyURL := os.Getenv("GOTIFY_URL") + gotifyToken := os.Getenv("GOTIFY_TOKEN") + + if gotifyURL == "" { + return fmt.Errorf("GOTIFY_URL environment variable is not set") + } + if gotifyToken == "" { + return fmt.Errorf("GOTIFY_TOKEN environment variable is not set") + } + + jsonData, err := json.Marshal(message) + if err != nil { + return fmt.Errorf("failed to marshal message: %w", err) + } + + url := fmt.Sprintf("%s/message?token=%s", gotifyURL, gotifyToken) + resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + return fmt.Errorf("failed to send message: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("gotify server returned status: %d", resp.StatusCode) + } + + return nil +} + +func sendMessage(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + message, err := request.RequireString("message") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + args, ok := request.Params.Arguments.(map[string]interface{}) + if !ok { + return mcp.NewToolResultError("invalid arguments type"), nil + } + + title := getStringArg(args, "title", "") + priority := getNumberArg(args, "priority", 5) + + gotifyMsg := GotifyMessage{ + Message: message, + Title: title, + Priority: int(priority), + } + + if err := sendGotifyMessage(gotifyMsg); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Failed to send message: %s", err)), nil + } + + return mcp.NewToolResultText("Message sent successfully"), nil +} + +func askForHelp(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + contextStr, err := request.RequireString("context") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + args, ok := request.Params.Arguments.(map[string]interface{}) + if !ok { + return mcp.NewToolResultError("invalid arguments type"), nil + } + + errorMsg := getStringArg(args, "error", "") + + message := fmt.Sprintf("🆘 Help needed: %s", contextStr) + if errorMsg != "" { + message += fmt.Sprintf("\nError: %s", errorMsg) + } + + gotifyMsg := GotifyMessage{ + Message: message, + Title: "Help Request", + Priority: 8, + } + + if err := sendGotifyMessage(gotifyMsg); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Failed to send help request: %s", err)), nil + } + + return mcp.NewToolResultText("Help request sent successfully"), nil +} + +func notifyCompletion(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + task, err := request.RequireString("task") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + args, ok := request.Params.Arguments.(map[string]interface{}) + if !ok { + return mcp.NewToolResultError("invalid arguments type"), nil + } + + result := getStringArg(args, "result", "") + + message := fmt.Sprintf("✅ Task completed: %s", task) + if result != "" { + message += fmt.Sprintf("\nResult: %s", result) + } + + gotifyMsg := GotifyMessage{ + Message: message, + Title: "Task Completed", + Priority: 6, + } + + if err := sendGotifyMessage(gotifyMsg); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Failed to send completion notification: %s", err)), nil + } + + return mcp.NewToolResultText("Completion notification sent successfully"), nil +} + +func summarizeActivity(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + summary, err := request.RequireString("summary") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + args, ok := request.Params.Arguments.(map[string]interface{}) + if !ok { + return mcp.NewToolResultError("invalid arguments type"), nil + } + + details := getStringArg(args, "details", "") + + message := fmt.Sprintf("📊 Activity Summary: %s", summary) + if details != "" { + message += fmt.Sprintf("\nDetails: %s", details) + } + + gotifyMsg := GotifyMessage{ + Message: message, + Title: "Activity Summary", + Priority: 4, + } + + if err := sendGotifyMessage(gotifyMsg); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Failed to send summary: %s", err)), nil + } + + return mcp.NewToolResultText("Activity summary sent successfully"), nil +} \ No newline at end of file