This commit is contained in:
Tai Groot
2025-07-22 03:15:18 -07:00
commit 4151e02f74
5 changed files with 380 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
main
gotify-mcp
.crush/

93
README.md Normal file
View File

@@ -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.

11
go.mod Normal file
View File

@@ -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
)

26
go.sum Normal file
View File

@@ -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=

247
main.go Normal file
View File

@@ -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
}