Metadata-Version: 2.4
Name: gotify-mcp
Version: 0.3.2
Summary: MCP server for Gotify push notifications
License-File: LICENSE
Requires-Python: >=3.12
Requires-Dist: fastmcp>=3.2.0
Requires-Dist: httpx>=0.28.1
Requires-Dist: python-dotenv>=1.1.0
Description-Content-Type: text/markdown

# Gotify MCP

<!-- mcp-name: tv.tootie/gotify-mcp -->

[![PyPI](https://img.shields.io/pypi/v/gotify-mcp)](https://pypi.org/project/gotify-mcp/) [![ghcr.io](https://img.shields.io/badge/ghcr.io-jmagar%2Fgotify--mcp-blue?logo=docker)](https://github.com/jmagar/gotify-mcp/pkgs/container/gotify-mcp)

MCP server for self-hosted Gotify. Exposes a unified `gotify` action router and a `gotify_help` companion tool for sending notifications and managing Gotify messages, applications, clients, and account metadata.

## Overview

Two MCP tools are exposed:

| Tool | Purpose |
| --- | --- |
| `gotify` | Unified action router for all Gotify operations |
| `gotify_help` | Returns markdown documentation for all actions and parameters |

The server supports HTTP (default) and stdio transports. HTTP transport requires bearer authentication via `GOTIFY_MCP_TOKEN`.

## What this repository ships

- `gotify_mcp/server.py`: FastMCP server, action router, and BearerAuth middleware
- `gotify_mcp/services/gotify.py`: Async HTTP client for the Gotify REST API
- `skills/gotify/SKILL.md`: Client-facing skill documentation
- `docs/gotify-api.json`: Bundled upstream Gotify API reference
- `.claude-plugin/plugin.json`, `.codex-plugin/plugin.json`, `gemini-extension.json`: Client manifests
- `docker-compose.yml`, `Dockerfile`, `entrypoint.sh`: Container deployment
- `scripts/`: Smoke tests and contract checks

## Tools

### `gotify`

Single entry point for all Gotify operations. Select the operation with the `action` parameter.

```python
gotify(action="send_message", app_token="AbCdEf", message="Build finished", priority=5)
```

### `gotify_help`

Returns the full action reference as Markdown. Call this to discover available actions.

```python
gotify_help()
```

## Actions

### `send_message`

Send a push notification. Requires an `app_token` — this is the per-application token, not the client token.

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `app_token` | string | yes | — | Application token from Gotify UI (Settings > Apps) |
| `message` | string | yes | — | Notification body. Supports Markdown when `extras` sets `contentType`. |
| `title` | string | no | — | Notification title |
| `priority` | integer | no | app default | Priority 0–10. See Priority Levels below. |
| `extras` | dict | no | — | Extended metadata. See Extras Structure below. |

Response fields:

| Field | Type | Description |
| --- | --- | --- |
| `id` | integer | Assigned message ID |
| `appid` | integer | Application ID that sent the message |
| `message` | string | Message body |
| `title` | string | Message title |
| `priority` | integer | Effective priority |
| `date` | string | ISO 8601 timestamp |
| `extras` | dict | Extras as submitted |

Example:

```python
gotify(action="send_message",
       app_token="AbCdEf",
       title="Deployment done",
       message="## Summary\n- All steps complete\n- Ready for review",
       priority=7,
       extras={"client::display": {"contentType": "text/markdown"}})
```

### `list_messages`

List messages with pagination and optional filtering.

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `app_id` | integer | no | — | Filter to messages from one application |
| `offset` | integer | no | `0` | Cursor offset (message ID) — items before this ID are skipped |
| `limit` | integer | no | `50` | Maximum number of messages to return |
| `sort_by` | string | no | `"id"` | Field to sort by. Valid values: `id`, `date`, `priority` |
| `sort_order` | string | no | `"desc"` | `"asc"` or `"desc"` |
| `query` | string | no | `""` | Case-insensitive substring filter applied to title and message body |

Response fields:

| Field | Type | Description |
| --- | --- | --- |
| `items` | array | Array of message objects (same shape as `send_message` response) |
| `total` | integer | Total messages before pagination |
| `limit` | integer | Limit used |
| `offset` | integer | Offset used |
| `has_more` | boolean | Whether more pages exist |

Note: Gotify uses cursor-style pagination internally. The `offset` parameter maps to the `since` query parameter (a message ID), not a row count.

Example:

```python
gotify(action="list_messages", limit=20, sort_order="desc")
gotify(action="list_messages", app_id=3, query="error", limit=10)
```

### `delete_message`

Delete a single message by ID. Destructive — requires `confirm=True`.

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `message_id` | integer | yes | — | ID of the message to delete |
| `confirm` | boolean | yes | `False` | Must be `True` to proceed |

Example:

```python
gotify(action="delete_message", message_id=42, confirm=True)
```

### `delete_all_messages`

Delete all messages across all applications. Destructive — requires `confirm=True`.

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `confirm` | boolean | yes | `False` | Must be `True` to proceed |

Example:

```python
gotify(action="delete_all_messages", confirm=True)
```

### `list_applications`

List all applications registered on the Gotify server.

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `offset` | integer | no | `0` | Number of items to skip |
| `limit` | integer | no | `50` | Maximum items to return |
| `query` | string | no | `""` | Case-insensitive substring filter on application name |

Response fields:

| Field | Type | Description |
| --- | --- | --- |
| `items` | array | Array of application objects |
| `total` | integer | Total applications before pagination |
| `limit` | integer | Limit used |
| `offset` | integer | Offset used |
| `has_more` | boolean | Whether more pages exist |

Each application object contains:

| Field | Type | Description |
| --- | --- | --- |
| `id` | integer | Application ID |
| `token` | string | Application token (use for `send_message`) |
| `name` | string | Application name |
| `description` | string | Application description |
| `defaultPriority` | integer | Default message priority |
| `image` | string | Path to application image |
| `internal` | boolean | Whether this is an internal application |

Example:

```python
gotify(action="list_applications")
gotify(action="list_applications", query="homelab")
```

### `create_application`

Create a new Gotify application.

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `name` | string | yes | — | Application name |
| `description` | string | no | — | Application description |
| `default_priority` | integer | no | — | Default priority for messages from this app (0–10) |

Returns the created application object.

Example:

```python
gotify(action="create_application",
       name="homelab-alerts",
       description="Claude Code homelab notifications",
       default_priority=5)
```

### `update_application`

Update an existing application. Provide at least one of `name`, `description`, or `default_priority`.

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `app_id` | integer | yes | — | ID of the application to update |
| `name` | string | no | — | New application name |
| `description` | string | no | — | New description |
| `default_priority` | integer | no | — | New default priority (0–10) |

Returns the updated application object.

Example:

```python
gotify(action="update_application", app_id=3, name="homelab-alerts-v2", default_priority=7)
```

### `delete_application`

Delete an application and all its messages. Destructive — requires `confirm=True`.

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `app_id` | integer | yes | — | ID of the application to delete |
| `confirm` | boolean | yes | `False` | Must be `True` to proceed |

Example:

```python
gotify(action="delete_application", app_id=3, confirm=True)
```

### `list_clients`

List all registered Gotify clients. Requires `GOTIFY_CLIENT_TOKEN`.

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `offset` | integer | no | `0` | Number of items to skip |
| `limit` | integer | no | `50` | Maximum items to return |
| `query` | string | no | `""` | Case-insensitive substring filter on client name |

Response has the same pagination shape as `list_applications`. Each client object contains `id`, `token`, and `name`.

Example:

```python
gotify(action="list_clients")
```

### `create_client`

Create a new Gotify client. Returns the client object including its token.

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `name` | string | yes | — | Client name |

Example:

```python
gotify(action="create_client", name="my-phone")
```

### `delete_client`

Delete a Gotify client. Destructive — requires `confirm=True`.

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `client_id` | integer | yes | — | ID of the client to delete |
| `confirm` | boolean | yes | `False` | Must be `True` to proceed |

Example:

```python
gotify(action="delete_client", client_id=5, confirm=True)
```

### `health`

Check the Gotify server health status. No additional parameters.

Returns a JSON object with health fields from the upstream Gotify `/health` endpoint. Note: this MCP tool call requires bearer authentication. The raw HTTP `/health` endpoint on the MCP server is unauthenticated.

Example:

```python
gotify(action="health")
```

### `version`

Get the Gotify server version. No additional parameters. No authentication required on the upstream call.

Example:

```python
gotify(action="version")
```

### `current_user`

Get the current authenticated user's account information. Requires `GOTIFY_CLIENT_TOKEN`.

No additional parameters. Returns the user object with `id`, `name`, and `admin` fields.

Example:

```python
gotify(action="current_user")
```

## Token Types

Gotify uses two separate token types. Using the wrong type will produce a 401 error.

| Token | Source | Used for |
| --- | --- | --- |
| **App token** | Gotify UI: Settings > Apps > Create Application | `send_message` only — passed per call as `app_token` |
| **Client token** | Gotify UI: Settings > Clients > Create Client | All management actions: list/delete messages, list/create/delete apps and clients, current_user |

The MCP server reads `GOTIFY_CLIENT_TOKEN` from the environment and uses it automatically for management actions. You never pass it explicitly to the tool.

The `app_token` for `send_message` is always passed explicitly per call — it is not read from the server environment.

## Priority Levels

The `priority` field is an integer from 0 to 10. Gotify clients interpret priority ranges as follows:

| Range | Level | Recommended use |
| --- | --- | --- |
| 0–3 | Low | Informational, FYI messages |
| 4–7 | Normal | Task updates, completions, standard alerts |
| 8–10 | High | Blocked states, errors, urgent alerts |

If `priority` is omitted from `send_message`, the application's `defaultPriority` is used. If the application has no default, Gotify falls back to 0.

## Extras Structure

The `extras` field in `send_message` is a free-form dict passed to the Gotify API. The most common use is enabling Markdown rendering:

```python
extras={"client::display": {"contentType": "text/markdown"}}
```

Other known namespaces from the upstream Gotify extras specification:

| Key | Value type | Description |
| --- | --- | --- |
| `client::display` | dict | Display hints for Gotify clients |
| `client::display.contentType` | string | `"text/plain"` (default) or `"text/markdown"` |
| `client::notification` | dict | Platform-specific notification overrides |

Any key/value pairs are accepted — the server passes them through as-is.

## Destructive Operations

Four actions are gated behind a confirmation check:

- `delete_message`
- `delete_all_messages`
- `delete_application`
- `delete_client`

Without `confirm=True`, the server returns:

```json
{"error": "Destructive operation. Pass confirm=True to proceed."}
```

To bypass the gate server-wide, set either environment variable:

```
ALLOW_DESTRUCTIVE=true   # skip confirm check
ALLOW_YOLO=true          # identical effect
```

These env vars are intended for automated environments where interactive confirmation is not possible.

## Pagination

List actions (`list_messages`, `list_applications`, `list_clients`) share a common pagination interface:

| Parameter | Type | Default | Notes |
| --- | --- | --- | --- |
| `offset` | integer | `0` | Items to skip. For `list_messages`, maps to the `since` cursor (a message ID). For `list_applications` and `list_clients`, applied client-side as a row offset. |
| `limit` | integer | `50` | Maximum items per page |
| `sort_by` | string | `"id"` | `list_messages` only. Field to sort by: `id`, `date`, `priority`. Not applied for apps or clients. |
| `sort_order` | string | `"desc"` | `list_messages` only. `"asc"` or `"desc"`. |
| `query` | string | `""` | Substring filter. Matches title and body for messages; name for apps and clients. Case-insensitive. |

All list responses include `total`, `limit`, `offset`, and `has_more` alongside the `items` array.

## Error Handling

All errors return a JSON object with these fields:

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Short error identifier |
| `errorCode` | integer | HTTP status code or 500 for network errors |
| `errorDescription` | string | Human-readable explanation |

Common errors:

| error | errorCode | Cause |
| --- | --- | --- |
| `Unauthorized` | 401 | Wrong or missing token type for the operation |
| `HTTP 403` | 403 | Token valid but operation not permitted for this user |
| `HTTP 404` | 404 | Message, application, or client ID does not exist |
| `NoUpdateFields` | 400 | `update_application` called with no fields to update |
| `RequestError` | 500 | Network failure reaching the Gotify server |
| `No token provided` | 401 | Neither `app_token` nor `GOTIFY_CLIENT_TOKEN` is set |

Responses are truncated at 512 KB. Truncated responses include `... [truncated]` at the end.

## Installation

### Marketplace

```bash
/plugin marketplace add jmagar/claude-homelab
/plugin install gotify-mcp @jmagar-claude-homelab
```

### Local development

```bash
uv sync --dev
uv run gotify-mcp-server
```

Direct module invocation:

```bash
uv run python -m gotify_mcp.server
```

### Docker

```bash
just up
```

Or manually:

```bash
docker compose up -d
```

## Configuration

Copy `.env.example` to `.env` and fill in the required values:

```bash
cp .env.example .env
```

### Environment variables

| Variable | Required | Default | Description |
| --- | --- | --- | --- |
| `GOTIFY_URL` | yes | — | Base URL of your Gotify server (no trailing slash). Server exits at startup if unset. |
| `GOTIFY_CLIENT_TOKEN` | yes* | — | Client token for management operations. Without this, all management actions fail. |
| `GOTIFY_APP_TOKEN` | no | — | App token used in HTTP fallback examples. The MCP tool requires `app_token` per call. |
| `GOTIFY_MCP_HOST` | no | `0.0.0.0` | Interface for the MCP HTTP server to bind to |
| `GOTIFY_MCP_PORT` | no | `9158` | Port for the MCP HTTP server |
| `GOTIFY_MCP_TRANSPORT` | no | `http` | Transport mode: `http` or `stdio` |
| `GOTIFY_MCP_TOKEN` | yes** | — | Bearer token for MCP server authentication. Generate with `openssl rand -hex 32`. Required when transport is `http` and `GOTIFY_MCP_NO_AUTH` is not set. |
| `GOTIFY_MCP_NO_AUTH` | no | `false` | Set `true` to disable bearer auth. Appropriate only behind a trusted reverse proxy. |
| `GOTIFY_LOG_LEVEL` | no | `INFO` | Log level: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` |
| `ALLOW_DESTRUCTIVE` | no | `false` | Set `true` to skip `confirm=True` requirement for destructive actions |
| `ALLOW_YOLO` | no | `false` | Identical to `ALLOW_DESTRUCTIVE` |
| `PUID` | no | `1000` | User ID for container process |
| `PGID` | no | `1000` | Group ID for container process |

*`GOTIFY_CLIENT_TOKEN` is required for management actions. Without it, a warning is logged at startup and management actions return 401.

**`GOTIFY_MCP_TOKEN` is required when `GOTIFY_MCP_TRANSPORT=http` and `GOTIFY_MCP_NO_AUTH=false`. The server exits at startup if neither is set.

### Docker URL rewriting

When running inside Docker, `localhost` and `127.0.0.1` in `GOTIFY_URL` are automatically rewritten to `host.docker.internal` so the container can reach a host-side Gotify server.

## Usage examples

### Send a plain text notification

```python
gotify(action="send_message",
       app_token="AbCdEf",
       title="Build finished",
       message="All tests passed.",
       priority=5)
```

### Send a Markdown notification

```python
gotify(action="send_message",
       app_token="AbCdEf",
       title="Deploy complete",
       message="## Status\n- All steps done\n- Ready for review",
       priority=7,
       extras={"client::display": {"contentType": "text/markdown"}})
```

### Page through messages

```python
# First page
gotify(action="list_messages", limit=25, offset=0)

# Next page (use the ID of the last message as offset)
gotify(action="list_messages", limit=25, offset=99)
```

### Filter messages by text

```python
gotify(action="list_messages", query="error", limit=20)
```

### Filter messages from one application

```python
gotify(action="list_messages", app_id=3, limit=50)
```

### Manage applications

```python
# List all applications
gotify(action="list_applications")

# Create
gotify(action="create_application",
       name="homelab-alerts",
       description="Automated notifications",
       default_priority=5)

# Update
gotify(action="update_application", app_id=3, default_priority=7)

# Delete (destructive)
gotify(action="delete_application", app_id=3, confirm=True)
```

### Manage clients

```python
# List all clients
gotify(action="list_clients")

# Create
gotify(action="create_client", name="my-phone")

# Delete (destructive)
gotify(action="delete_client", client_id=5, confirm=True)
```

### Server info

```python
gotify(action="health")
gotify(action="version")
gotify(action="current_user")
```

## HTTP fallback

When MCP tools are unavailable, use direct HTTP calls. App tokens go to `/message`, client tokens go to management endpoints.

```bash
# Send a notification
curl -s -X POST "$GOTIFY_URL/message" \
  -H "X-Gotify-Key: $GOTIFY_APP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"Done","message":"All steps complete","priority":7}'

# List messages
curl -s "$GOTIFY_URL/message" \
  -H "X-Gotify-Key: $GOTIFY_CLIENT_TOKEN"

# List applications
curl -s "$GOTIFY_URL/application" \
  -H "X-Gotify-Key: $GOTIFY_CLIENT_TOKEN"

# Health (no auth)
curl -s "$GOTIFY_URL/health"
```

## Development

### Setup

```bash
just setup
```

This copies `.env.example` to `.env` (if not already present) and installs all dependencies.

### Commands

```bash
just dev          # Run the server locally (uv run python -m gotify_mcp.server)
just lint         # Run ruff check
just fmt          # Run ruff format
just typecheck    # Run ty check
just test         # Run pytest
just build        # Build Docker image
just up           # Start via docker compose
just down         # Stop docker compose
just restart      # Restart docker compose
just logs         # Follow docker compose logs
just health       # curl http://localhost:9158/health
just test-live    # Run live integration tests (requires running server)
just gen-token    # Generate a random bearer token
just clean        # Remove build artifacts
```

## Verification

Run before committing:

```bash
just lint
just typecheck
just test
```

Live verification (requires a running server and Gotify instance):

```bash
just test-live
```

## Server health endpoint

The MCP server exposes an unauthenticated HTTP health endpoint:

```
GET http://localhost:9158/health
```

This proxies through to the Gotify server's `/health` and returns:

```json
{"status": "ok", "gotify": {...}}
```

Or on failure:

```json
{"status": "error", "reason": "..."}
```

## Logs

The server writes rotating logs to `logs/gotify_mcp.log` (max 5 MB, 3 backups). Log level is controlled by `GOTIFY_LOG_LEVEL`.

## Related plugins

| Plugin | Category | Description |
|--------|----------|-------------|
| [homelab-core](https://github.com/jmagar/claude-homelab) | core | Core agents, commands, skills, and setup/health workflows for homelab management. |
| [overseerr-mcp](https://github.com/jmagar/overseerr-mcp) | media | Search movies and TV shows, submit requests, and monitor failed requests via Overseerr. |
| [unraid-mcp](https://github.com/jmagar/unraid-mcp) | infrastructure | Query, monitor, and manage Unraid servers: Docker, VMs, array, parity, and live telemetry. |
| [unifi-mcp](https://github.com/jmagar/unifi-mcp) | infrastructure | Monitor and manage UniFi devices, clients, firewall rules, and network health. |
| [swag-mcp](https://github.com/jmagar/swag-mcp) | infrastructure | Create, edit, and manage SWAG nginx reverse proxy configurations. |
| [synapse-mcp](https://github.com/jmagar/synapse-mcp) | infrastructure | Docker management (Flux) and SSH remote operations (Scout) across homelab hosts. |
| [arcane-mcp](https://github.com/jmagar/arcane-mcp) | infrastructure | Manage Docker environments, containers, images, volumes, networks, and GitOps via Arcane. |
| [syslog-mcp](https://github.com/jmagar/syslog-mcp) | infrastructure | Receive, index, and search syslog streams from all homelab hosts via SQLite FTS5. |
| [plugin-lab](https://github.com/jmagar/plugin-lab) | dev-tools | Scaffold, review, align, and deploy homelab MCP plugins with agents and canonical templates. |
| [axon](https://github.com/jmagar/axon) | research | Self-hosted web crawl, ingest, embed, and RAG pipeline with MCP tooling. |

## License

MIT
