gotify-mcp 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gotify_mcp/__init__.py +1 -0
- gotify_mcp/server.py +459 -0
- gotify_mcp/services/__init__.py +1 -0
- gotify_mcp/services/gotify.py +328 -0
- gotify_mcp-0.3.0.dist-info/METADATA +696 -0
- gotify_mcp-0.3.0.dist-info/RECORD +9 -0
- gotify_mcp-0.3.0.dist-info/WHEEL +4 -0
- gotify_mcp-0.3.0.dist-info/entry_points.txt +2 -0
- gotify_mcp-0.3.0.dist-info/licenses/LICENSE +21 -0
gotify_mcp/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Gotify MCP Server — FastMCP server for Gotify push notifications."""
|
gotify_mcp/server.py
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gotify MCP Server — two-tool design with BearerAuth middleware.
|
|
3
|
+
|
|
4
|
+
Tools:
|
|
5
|
+
gotify — all operations via action + subaction routing
|
|
6
|
+
gotify_help — returns markdown help for all actions
|
|
7
|
+
|
|
8
|
+
Transport: GOTIFY_MCP_TRANSPORT=http (default) | stdio
|
|
9
|
+
Auth: GOTIFY_MCP_TOKEN (required for HTTP; skipped for stdio)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import hmac
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
import logging.handlers
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Literal
|
|
22
|
+
|
|
23
|
+
from dotenv import load_dotenv
|
|
24
|
+
from fastmcp import FastMCP
|
|
25
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
26
|
+
from starlette.requests import Request
|
|
27
|
+
from starlette.responses import JSONResponse
|
|
28
|
+
|
|
29
|
+
from gotify_mcp.services.gotify import GotifyClient
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Environment loading
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
PACKAGE_DIR = Path(__file__).resolve().parent
|
|
35
|
+
REPO_ROOT = PACKAGE_DIR.parent
|
|
36
|
+
load_dotenv(dotenv_path=REPO_ROOT / ".env")
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# Logging
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
GOTIFY_LOG_LEVEL_STR = os.getenv(
|
|
42
|
+
"GOTIFY_LOG_LEVEL", os.getenv("LOG_LEVEL", "INFO")
|
|
43
|
+
).upper()
|
|
44
|
+
NUMERIC_LOG_LEVEL = getattr(logging, GOTIFY_LOG_LEVEL_STR, logging.INFO)
|
|
45
|
+
|
|
46
|
+
logger = logging.getLogger("GotifyMCPServer")
|
|
47
|
+
logger.setLevel(NUMERIC_LOG_LEVEL)
|
|
48
|
+
logger.propagate = False
|
|
49
|
+
|
|
50
|
+
_console = logging.StreamHandler(sys.stdout)
|
|
51
|
+
_console.setLevel(NUMERIC_LOG_LEVEL)
|
|
52
|
+
_console.setFormatter(
|
|
53
|
+
logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
|
54
|
+
)
|
|
55
|
+
if not any(isinstance(h, logging.StreamHandler) for h in logger.handlers):
|
|
56
|
+
logger.addHandler(_console)
|
|
57
|
+
|
|
58
|
+
_logs_dir = REPO_ROOT / "logs"
|
|
59
|
+
_logs_dir.mkdir(exist_ok=True)
|
|
60
|
+
_file_handler = logging.handlers.RotatingFileHandler(
|
|
61
|
+
_logs_dir / "gotify_mcp.log",
|
|
62
|
+
maxBytes=5 * 1024 * 1024,
|
|
63
|
+
backupCount=3,
|
|
64
|
+
encoding="utf-8",
|
|
65
|
+
)
|
|
66
|
+
_file_handler.setLevel(NUMERIC_LOG_LEVEL)
|
|
67
|
+
_file_handler.setFormatter(
|
|
68
|
+
logging.Formatter(
|
|
69
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(module)s - %(funcName)s - %(lineno)d - %(message)s"
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
if not any(
|
|
73
|
+
isinstance(h, logging.handlers.RotatingFileHandler) for h in logger.handlers
|
|
74
|
+
):
|
|
75
|
+
logger.addHandler(_file_handler)
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# Configuration & startup validation
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
GOTIFY_URL = os.getenv("GOTIFY_URL", "")
|
|
81
|
+
GOTIFY_CLIENT_TOKEN = os.getenv("GOTIFY_CLIENT_TOKEN")
|
|
82
|
+
GOTIFY_MCP_TOKEN = os.getenv("GOTIFY_MCP_TOKEN")
|
|
83
|
+
GOTIFY_MCP_NO_AUTH = os.getenv("GOTIFY_MCP_NO_AUTH", "").lower() in ("true", "1", "yes")
|
|
84
|
+
GOTIFY_MCP_TRANSPORT = os.getenv("GOTIFY_MCP_TRANSPORT", "http").lower()
|
|
85
|
+
GOTIFY_MCP_HOST = os.getenv("GOTIFY_MCP_HOST", "0.0.0.0")
|
|
86
|
+
GOTIFY_MCP_PORT = int(os.getenv("GOTIFY_MCP_PORT", "8084"))
|
|
87
|
+
|
|
88
|
+
# Destructive operations gates
|
|
89
|
+
ALLOW_DESTRUCTIVE = os.getenv("ALLOW_DESTRUCTIVE", "false").lower() == "true"
|
|
90
|
+
ALLOW_YOLO = os.getenv("ALLOW_YOLO", "false").lower() == "true"
|
|
91
|
+
|
|
92
|
+
if not GOTIFY_URL:
|
|
93
|
+
print("CRITICAL: GOTIFY_URL must be set in the environment.", file=sys.stderr)
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
|
|
96
|
+
if GOTIFY_MCP_TRANSPORT != "stdio" and not GOTIFY_MCP_NO_AUTH and not GOTIFY_MCP_TOKEN:
|
|
97
|
+
print(
|
|
98
|
+
"CRITICAL: GOTIFY_MCP_TOKEN is not set.\n"
|
|
99
|
+
"Set GOTIFY_MCP_TOKEN to a secure random token, or set GOTIFY_MCP_NO_AUTH=true\n"
|
|
100
|
+
"to disable auth (only appropriate when secured at the network/proxy level).\n\n"
|
|
101
|
+
"Generate a token with: openssl rand -hex 32",
|
|
102
|
+
file=sys.stderr,
|
|
103
|
+
)
|
|
104
|
+
sys.exit(1)
|
|
105
|
+
|
|
106
|
+
if not GOTIFY_CLIENT_TOKEN:
|
|
107
|
+
logger.warning("GOTIFY_CLIENT_TOKEN is not set. Management actions will fail.")
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
# Response size cap
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
MAX_RESPONSE_SIZE = 512 * 1024 # 512 KB
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def truncate_response(text: str) -> str:
|
|
116
|
+
if len(text.encode()) > MAX_RESPONSE_SIZE:
|
|
117
|
+
return text[:MAX_RESPONSE_SIZE] + "\n... [truncated]"
|
|
118
|
+
return text
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _json(obj: Any) -> str:
|
|
122
|
+
return truncate_response(json.dumps(obj, default=str))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
# Gotify API client (singleton)
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
_client = GotifyClient(base_url=GOTIFY_URL, client_token=GOTIFY_CLIENT_TOKEN)
|
|
129
|
+
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
# FastMCP server + BearerAuth middleware
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
mcp = FastMCP(
|
|
134
|
+
name="Gotify MCP Server",
|
|
135
|
+
instructions=(
|
|
136
|
+
"Interact with Gotify push notification server. "
|
|
137
|
+
"Call gotify_help to see all available actions."
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class BearerAuthMiddleware(BaseHTTPMiddleware):
|
|
143
|
+
async def dispatch(self, request: Request, call_next):
|
|
144
|
+
if GOTIFY_MCP_NO_AUTH:
|
|
145
|
+
return await call_next(request)
|
|
146
|
+
# /health is always unauthenticated
|
|
147
|
+
if request.url.path in ("/health",):
|
|
148
|
+
return await call_next(request)
|
|
149
|
+
auth = request.headers.get("Authorization", "")
|
|
150
|
+
if not auth.startswith("Bearer "):
|
|
151
|
+
return JSONResponse({"error": "Unauthorized"}, status_code=401)
|
|
152
|
+
provided = auth[7:]
|
|
153
|
+
# Timing-safe comparison
|
|
154
|
+
if not hmac.compare_digest(provided, GOTIFY_MCP_TOKEN or ""):
|
|
155
|
+
return JSONResponse({"error": "Unauthorized"}, status_code=401)
|
|
156
|
+
return await call_next(request)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
# Destructive ops gate
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _destructive_gate(confirm: bool) -> str | None:
|
|
165
|
+
"""Return an error string if the destructive op should be blocked, else None."""
|
|
166
|
+
if ALLOW_YOLO or ALLOW_DESTRUCTIVE:
|
|
167
|
+
return None
|
|
168
|
+
if not confirm:
|
|
169
|
+
return "Destructive operation. Pass confirm=True to proceed."
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
# Tool: gotify
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
GOTIFY_ACTIONS = Literal[
|
|
178
|
+
"send_message",
|
|
179
|
+
"list_messages",
|
|
180
|
+
"delete_message",
|
|
181
|
+
"delete_all_messages",
|
|
182
|
+
"list_applications",
|
|
183
|
+
"create_application",
|
|
184
|
+
"update_application",
|
|
185
|
+
"delete_application",
|
|
186
|
+
"list_clients",
|
|
187
|
+
"create_client",
|
|
188
|
+
"delete_client",
|
|
189
|
+
"health",
|
|
190
|
+
"version",
|
|
191
|
+
"current_user",
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@mcp.tool()
|
|
196
|
+
async def gotify(
|
|
197
|
+
action: GOTIFY_ACTIONS,
|
|
198
|
+
subaction: str = "",
|
|
199
|
+
# Message params
|
|
200
|
+
app_token: str = "",
|
|
201
|
+
message: str = "",
|
|
202
|
+
title: str = "",
|
|
203
|
+
priority: int | None = None,
|
|
204
|
+
message_id: int | None = None,
|
|
205
|
+
extras: dict[str, Any] | None = None,
|
|
206
|
+
# Application params
|
|
207
|
+
app_id: int | None = None,
|
|
208
|
+
name: str = "",
|
|
209
|
+
description: str = "",
|
|
210
|
+
default_priority: int | None = None,
|
|
211
|
+
# Client params
|
|
212
|
+
client_id: int | None = None,
|
|
213
|
+
# Pagination / filtering
|
|
214
|
+
offset: int = 0,
|
|
215
|
+
limit: int = 50,
|
|
216
|
+
sort_by: str = "id",
|
|
217
|
+
sort_order: str = "desc",
|
|
218
|
+
query: str = "",
|
|
219
|
+
# Destructive gate
|
|
220
|
+
confirm: bool = False,
|
|
221
|
+
) -> str:
|
|
222
|
+
"""Gotify MCP tool — routes all actions to the Gotify push notification API.
|
|
223
|
+
|
|
224
|
+
Actions:
|
|
225
|
+
send_message — send a notification (requires app_token, message)
|
|
226
|
+
list_messages — list messages (optional: app_id, offset, limit, sort_by, sort_order, query)
|
|
227
|
+
delete_message — delete one message (requires message_id) [destructive]
|
|
228
|
+
delete_all_messages — delete all messages [destructive]
|
|
229
|
+
list_applications — list apps (optional: offset, limit, query)
|
|
230
|
+
create_application — create app (requires name; optional: description, default_priority)
|
|
231
|
+
update_application — update app (requires app_id; optional: name, description, default_priority)
|
|
232
|
+
delete_application — delete app (requires app_id) [destructive]
|
|
233
|
+
list_clients — list clients (optional: offset, limit, query)
|
|
234
|
+
create_client — create client (requires name)
|
|
235
|
+
delete_client — delete client (requires client_id) [destructive]
|
|
236
|
+
health — Gotify server health check
|
|
237
|
+
version — Gotify server version
|
|
238
|
+
current_user — current authenticated user info
|
|
239
|
+
"""
|
|
240
|
+
match action:
|
|
241
|
+
case "send_message":
|
|
242
|
+
if not app_token:
|
|
243
|
+
return _json({"error": "app_token is required for send_message"})
|
|
244
|
+
if not message:
|
|
245
|
+
return _json({"error": "message is required for send_message"})
|
|
246
|
+
result = await _client.send_message(
|
|
247
|
+
app_token=app_token,
|
|
248
|
+
message=message,
|
|
249
|
+
title=title or None,
|
|
250
|
+
priority=priority,
|
|
251
|
+
extras=extras,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
case "list_messages":
|
|
255
|
+
result = await _client.list_messages(
|
|
256
|
+
app_id=app_id,
|
|
257
|
+
offset=offset,
|
|
258
|
+
limit=limit,
|
|
259
|
+
sort_by=sort_by,
|
|
260
|
+
sort_order=sort_order,
|
|
261
|
+
query=query,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
case "delete_message":
|
|
265
|
+
if (err := _destructive_gate(confirm)) is not None:
|
|
266
|
+
return err
|
|
267
|
+
if message_id is None:
|
|
268
|
+
return _json({"error": "message_id is required for delete_message"})
|
|
269
|
+
result = await _client.delete_message(message_id)
|
|
270
|
+
|
|
271
|
+
case "delete_all_messages":
|
|
272
|
+
if (err := _destructive_gate(confirm)) is not None:
|
|
273
|
+
return err
|
|
274
|
+
result = await _client.delete_all_messages()
|
|
275
|
+
|
|
276
|
+
case "list_applications":
|
|
277
|
+
result = await _client.list_applications(
|
|
278
|
+
offset=offset, limit=limit, query=query
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
case "create_application":
|
|
282
|
+
if not name:
|
|
283
|
+
return _json({"error": "name is required for create_application"})
|
|
284
|
+
result = await _client.create_application(
|
|
285
|
+
name=name,
|
|
286
|
+
description=description or None,
|
|
287
|
+
default_priority=default_priority,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
case "update_application":
|
|
291
|
+
if app_id is None:
|
|
292
|
+
return _json({"error": "app_id is required for update_application"})
|
|
293
|
+
result = await _client.update_application(
|
|
294
|
+
app_id=app_id,
|
|
295
|
+
name=name or None,
|
|
296
|
+
description=description or None,
|
|
297
|
+
default_priority=default_priority,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
case "delete_application":
|
|
301
|
+
if (err := _destructive_gate(confirm)) is not None:
|
|
302
|
+
return err
|
|
303
|
+
if app_id is None:
|
|
304
|
+
return _json({"error": "app_id is required for delete_application"})
|
|
305
|
+
result = await _client.delete_application(app_id)
|
|
306
|
+
|
|
307
|
+
case "list_clients":
|
|
308
|
+
result = await _client.list_clients(offset=offset, limit=limit, query=query)
|
|
309
|
+
|
|
310
|
+
case "create_client":
|
|
311
|
+
if not name:
|
|
312
|
+
return _json({"error": "name is required for create_client"})
|
|
313
|
+
result = await _client.create_client(name)
|
|
314
|
+
|
|
315
|
+
case "delete_client":
|
|
316
|
+
if (err := _destructive_gate(confirm)) is not None:
|
|
317
|
+
return err
|
|
318
|
+
if client_id is None:
|
|
319
|
+
return _json({"error": "client_id is required for delete_client"})
|
|
320
|
+
result = await _client.delete_client(client_id)
|
|
321
|
+
|
|
322
|
+
case "health":
|
|
323
|
+
result = await _client.get_health()
|
|
324
|
+
|
|
325
|
+
case "version":
|
|
326
|
+
result = await _client.get_version()
|
|
327
|
+
|
|
328
|
+
case "current_user":
|
|
329
|
+
result = await _client.get_current_user()
|
|
330
|
+
|
|
331
|
+
case _:
|
|
332
|
+
return _json(
|
|
333
|
+
{"error": f"Unknown action: {action}. Call gotify_help for reference."}
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
return _json(result)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
# ---------------------------------------------------------------------------
|
|
340
|
+
# Tool: gotify_help
|
|
341
|
+
# ---------------------------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
_HELP_TEXT = """# Gotify MCP Server
|
|
344
|
+
|
|
345
|
+
Interact with a Gotify push notification server.
|
|
346
|
+
|
|
347
|
+
## Tool: `gotify`
|
|
348
|
+
|
|
349
|
+
Single entry point for all operations. Use `action` to select the operation.
|
|
350
|
+
|
|
351
|
+
### Actions
|
|
352
|
+
|
|
353
|
+
| Action | Description | Required params | Destructive |
|
|
354
|
+
|--------|-------------|-----------------|-------------|
|
|
355
|
+
| `send_message` | Send a push notification | `app_token`, `message` | no |
|
|
356
|
+
| `list_messages` | List messages | — | no |
|
|
357
|
+
| `delete_message` | Delete one message | `message_id` | yes |
|
|
358
|
+
| `delete_all_messages` | Delete all messages | — | yes |
|
|
359
|
+
| `list_applications` | List applications | — | no |
|
|
360
|
+
| `create_application` | Create an application | `name` | no |
|
|
361
|
+
| `update_application` | Update an application | `app_id` | no |
|
|
362
|
+
| `delete_application` | Delete an application | `app_id` | yes |
|
|
363
|
+
| `list_clients` | List clients | — | no |
|
|
364
|
+
| `create_client` | Create a client | `name` | no |
|
|
365
|
+
| `delete_client` | Delete a client | `client_id` | yes |
|
|
366
|
+
| `health` | Gotify server health | — | no |
|
|
367
|
+
| `version` | Gotify server version | — | no |
|
|
368
|
+
| `current_user` | Current user info | — | no |
|
|
369
|
+
|
|
370
|
+
### Pagination parameters (list actions)
|
|
371
|
+
|
|
372
|
+
| Param | Default | Description |
|
|
373
|
+
|-------|---------|-------------|
|
|
374
|
+
| `offset` | `0` | Number of items to skip |
|
|
375
|
+
| `limit` | `50` | Max items to return |
|
|
376
|
+
| `sort_by` | `"id"` | Field to sort by |
|
|
377
|
+
| `sort_order` | `"desc"` | `"asc"` or `"desc"` |
|
|
378
|
+
| `query` | `""` | Filter by text match |
|
|
379
|
+
|
|
380
|
+
### Destructive operations
|
|
381
|
+
|
|
382
|
+
Pass `confirm=True` to execute destructive actions (delete_message, delete_all_messages,
|
|
383
|
+
delete_application, delete_client).
|
|
384
|
+
|
|
385
|
+
Set `ALLOW_DESTRUCTIVE=true` or `ALLOW_YOLO=true` in the server environment to skip
|
|
386
|
+
the confirmation gate entirely.
|
|
387
|
+
|
|
388
|
+
### Examples
|
|
389
|
+
|
|
390
|
+
```
|
|
391
|
+
gotify(action="send_message", app_token="AbCdEf", message="Hello", title="Test")
|
|
392
|
+
gotify(action="list_messages", limit=10, sort_order="desc")
|
|
393
|
+
gotify(action="list_messages", app_id=3, query="error")
|
|
394
|
+
gotify(action="delete_message", message_id=42, confirm=True)
|
|
395
|
+
gotify(action="list_applications")
|
|
396
|
+
gotify(action="create_application", name="MyApp", description="Test app")
|
|
397
|
+
gotify(action="health")
|
|
398
|
+
```
|
|
399
|
+
"""
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
@mcp.tool()
|
|
403
|
+
async def gotify_help() -> str:
|
|
404
|
+
"""Returns markdown help for all Gotify MCP actions and parameters."""
|
|
405
|
+
return _HELP_TEXT
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# ---------------------------------------------------------------------------
|
|
409
|
+
# /health endpoint (unauthenticated)
|
|
410
|
+
# ---------------------------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
@mcp.custom_route("/health", methods=["GET"])
|
|
414
|
+
async def mcp_server_health(request: Request) -> JSONResponse:
|
|
415
|
+
"""MCP server health check."""
|
|
416
|
+
if not GOTIFY_URL:
|
|
417
|
+
return JSONResponse(
|
|
418
|
+
{"status": "error", "reason": "GOTIFY_URL not configured."},
|
|
419
|
+
status_code=500,
|
|
420
|
+
)
|
|
421
|
+
health = await _client.get_health()
|
|
422
|
+
if "error" in health:
|
|
423
|
+
return JSONResponse(
|
|
424
|
+
{"status": "error", "reason": health["error"]},
|
|
425
|
+
status_code=503,
|
|
426
|
+
)
|
|
427
|
+
return JSONResponse({"status": "ok", "gotify": health})
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
# ---------------------------------------------------------------------------
|
|
431
|
+
# Entry point
|
|
432
|
+
# ---------------------------------------------------------------------------
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def main() -> None:
|
|
436
|
+
logger.info(
|
|
437
|
+
"Starting Gotify MCP Server — transport=%s host=%s port=%s auth=%s",
|
|
438
|
+
GOTIFY_MCP_TRANSPORT,
|
|
439
|
+
GOTIFY_MCP_HOST,
|
|
440
|
+
GOTIFY_MCP_PORT,
|
|
441
|
+
"disabled" if GOTIFY_MCP_NO_AUTH else "bearer",
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
if GOTIFY_MCP_TRANSPORT == "stdio":
|
|
445
|
+
mcp.run(transport="stdio")
|
|
446
|
+
else:
|
|
447
|
+
# BearerAuth skipped when GOTIFY_MCP_NO_AUTH=true (OAuth handled at gateway)
|
|
448
|
+
if not GOTIFY_MCP_NO_AUTH:
|
|
449
|
+
mcp.app.add_middleware(BearerAuthMiddleware)
|
|
450
|
+
mcp.run(
|
|
451
|
+
transport="streamable-http",
|
|
452
|
+
host=GOTIFY_MCP_HOST,
|
|
453
|
+
port=GOTIFY_MCP_PORT,
|
|
454
|
+
log_level=GOTIFY_LOG_LEVEL_STR.lower(),
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
if __name__ == "__main__":
|
|
459
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Gotify MCP services layer
|