@davidfuchs/mcp-uptime-kuma 0.6.4 → 0.9.0
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.
- package/README.md +113 -439
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +745 -11
- package/dist/server.js.map +1 -1
- package/dist/types/docker-host.d.ts +26 -0
- package/dist/types/docker-host.d.ts.map +1 -0
- package/dist/types/docker-host.js +13 -0
- package/dist/types/docker-host.js.map +1 -0
- package/dist/types/heartbeat.d.ts +31 -31
- package/dist/types/heartbeat.d.ts.map +1 -1
- package/dist/types/heartbeat.js +1 -1
- package/dist/types/heartbeat.js.map +1 -1
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/maintenance.d.ts +70 -0
- package/dist/types/maintenance.d.ts.map +1 -0
- package/dist/types/maintenance.js +22 -0
- package/dist/types/maintenance.js.map +1 -0
- package/dist/types/notification.d.ts +29 -0
- package/dist/types/notification.d.ts.map +1 -0
- package/dist/types/notification.js +14 -0
- package/dist/types/notification.js.map +1 -0
- package/dist/types/status-page.d.ts +46 -0
- package/dist/types/status-page.d.ts.map +1 -0
- package/dist/types/status-page.js +19 -0
- package/dist/types/status-page.js.map +1 -0
- package/dist/uptime-kuma-client.d.ts +210 -1
- package/dist/uptime-kuma-client.d.ts.map +1 -1
- package/dist/uptime-kuma-client.js +637 -4
- package/dist/uptime-kuma-client.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -3
package/dist/server.js
CHANGED
|
@@ -2,7 +2,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
2
2
|
import { McpError, ErrorCode, SetLevelRequestSchema, LoggingLevelSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { UptimeKumaClient } from './uptime-kuma-client.js';
|
|
5
|
-
import { HeartbeatSchema, MonitorBaseSchema, MonitorSummarySchema, SettingsSchema } from './types/index.js';
|
|
5
|
+
import { HeartbeatSchema, MonitorBaseSchema, MonitorSummarySchema, SettingsSchema, NotificationSchema, MaintenanceSchema, StatusPageSchema, DockerHostSchema } from './types/index.js';
|
|
6
6
|
import { VERSION } from './version.js';
|
|
7
7
|
/**
|
|
8
8
|
* Creates and configures the MCP server with tools, resources, and prompts
|
|
@@ -16,11 +16,27 @@ export async function createServer(config) {
|
|
|
16
16
|
version: VERSION,
|
|
17
17
|
}, {
|
|
18
18
|
instructions: `
|
|
19
|
-
This MCP server provides access to Uptime Kuma monitoring data
|
|
19
|
+
This MCP server provides access to Uptime Kuma monitoring data and management operations.
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
Use '
|
|
21
|
+
READ operations:
|
|
22
|
+
- START with 'getMonitorSummary' for status overview ("how is everything?", "what's down?").
|
|
23
|
+
- Use 'getHeartbeats' or 'listHeartbeats' for historical data (limit to 5-10 heartbeats unless user requests more).
|
|
24
|
+
- Use 'listMonitors' when you need configuration details (URLs, intervals, notification settings).
|
|
25
|
+
- Use 'listNotifications' to see notification channels.
|
|
26
|
+
- Use 'listTags' to see available tags.
|
|
27
|
+
- Use 'getMaintenanceWindows' to see scheduled maintenance.
|
|
28
|
+
- Use 'listStatusPages' to see status page configurations, or 'getStatusPage' for one page's full details (groups + monitors).
|
|
29
|
+
- Use 'listDockerHosts' to see configured docker daemons (used by docker container monitors).
|
|
30
|
+
|
|
31
|
+
WRITE operations:
|
|
32
|
+
- Use 'createMonitor' / 'updateMonitor' / 'deleteMonitor' to manage monitors.
|
|
33
|
+
- Use 'addNotification' / 'updateNotification' / 'deleteNotification' to manage notification channels.
|
|
34
|
+
- Use 'addTag' / 'deleteTag' to manage tags.
|
|
35
|
+
- Use 'createMaintenance' to schedule a maintenance window.
|
|
36
|
+
- Use 'addDockerHost' / 'updateDockerHost' / 'deleteDockerHost' to manage docker daemon connections.
|
|
37
|
+
- Use 'testDockerHost' to verify a docker daemon is reachable before saving.
|
|
38
|
+
- Use 'createStatusPage' / 'updateStatusPage' / 'deleteStatusPage' to manage status pages. Creating returns an empty page — follow up with updateStatusPage to set groups and monitors.
|
|
39
|
+
- Use 'pauseMonitor' / 'resumeMonitor' to temporarily stop/start checks.
|
|
24
40
|
`,
|
|
25
41
|
capabilities: {
|
|
26
42
|
logging: {}
|
|
@@ -71,7 +87,7 @@ export async function createServer(config) {
|
|
|
71
87
|
title: 'Get Monitor',
|
|
72
88
|
description: 'Retrieves configuration details for a specific monitor by ID (URL, check interval, notification settings, etc.). Use this when you need to examine or modify settings for a specific monitor. For current status, use getMonitorSummary instead. By default returns only common fields plus runtime data (uptime, avgPing); set includeTypeSpecificFields to true to include type-specific fields (e.g., url for HTTP, hostname/port for TCP).',
|
|
73
89
|
inputSchema: {
|
|
74
|
-
monitorID: z.number().int().nonnegative().describe('The ID of the monitor to retrieve'),
|
|
90
|
+
monitorID: z.coerce.number().int().nonnegative().describe('The ID of the monitor to retrieve'),
|
|
75
91
|
includeTypeSpecificFields: z.boolean().optional().describe('Include type-specific fields (url, hostname, port, etc.) in addition to common fields. Default: false. When false, only returns MonitorBase fields plus uptime/avgPing.')
|
|
76
92
|
},
|
|
77
93
|
outputSchema: {
|
|
@@ -232,8 +248,8 @@ export async function createServer(config) {
|
|
|
232
248
|
title: 'Get Heartbeats',
|
|
233
249
|
description: 'Retrieves historical heartbeat data for a specific monitor (response times, status changes over time). Use this for analyzing patterns or history for one monitor. By default returns only the most recent heartbeat; set maxHeartbeats (up to 100) for historical analysis. Keep maxHeartbeats ≤10 unless user requests more.',
|
|
234
250
|
inputSchema: {
|
|
235
|
-
monitorID: z.number().int().nonnegative().describe('The ID of the monitor to get heartbeats for'),
|
|
236
|
-
maxHeartbeats: z.number().int().positive().max(100).optional().describe('If set, returns the most recent X heartbeats (up to 100). If unset, returns only the most recent heartbeat (default: 1)')
|
|
251
|
+
monitorID: z.coerce.number().int().nonnegative().describe('The ID of the monitor to get heartbeats for'),
|
|
252
|
+
maxHeartbeats: z.coerce.number().int().positive().max(100).optional().describe('If set, returns the most recent X heartbeats (up to 100). If unset, returns only the most recent heartbeat (default: 1)')
|
|
237
253
|
},
|
|
238
254
|
outputSchema: {
|
|
239
255
|
monitorID: z.number(),
|
|
@@ -296,7 +312,7 @@ export async function createServer(config) {
|
|
|
296
312
|
title: 'List Heartbeats',
|
|
297
313
|
description: 'Retrieves historical heartbeat data for ALL monitors (response times, status changes over time). Use this for analyzing patterns across multiple monitors or correlating events. By default returns only the most recent heartbeat per monitor; set maxHeartbeats (up to 100) for historical analysis. Keep maxHeartbeats ≤5 unless user requests more.',
|
|
298
314
|
inputSchema: {
|
|
299
|
-
maxHeartbeats: z.number().int().positive().max(100).optional().describe('If set, returns the most recent X heartbeats per monitor (up to 100). If unset, returns only the most recent heartbeat per monitor (default: 1)')
|
|
315
|
+
maxHeartbeats: z.coerce.number().int().positive().max(100).optional().describe('If set, returns the most recent X heartbeats per monitor (up to 100). If unset, returns only the most recent heartbeat per monitor (default: 1)')
|
|
300
316
|
},
|
|
301
317
|
outputSchema: {
|
|
302
318
|
heartbeats: z.record(z.string(), z.array(HeartbeatSchema)).describe('Map of monitor IDs to their heartbeat arrays'),
|
|
@@ -334,7 +350,7 @@ export async function createServer(config) {
|
|
|
334
350
|
title: 'Pause Monitor',
|
|
335
351
|
description: 'Pauses a monitor, stopping it from performing checks. The monitor will remain in the system but will not send notifications or collect data until resumed.',
|
|
336
352
|
inputSchema: {
|
|
337
|
-
monitorID: z.number().int().nonnegative().describe('The ID of the monitor to pause')
|
|
353
|
+
monitorID: z.coerce.number().int().nonnegative().describe('The ID of the monitor to pause')
|
|
338
354
|
},
|
|
339
355
|
outputSchema: {
|
|
340
356
|
ok: z.boolean(),
|
|
@@ -367,7 +383,7 @@ export async function createServer(config) {
|
|
|
367
383
|
title: 'Resume Monitor',
|
|
368
384
|
description: 'Resumes a paused monitor, restarting all checks. Use this to re-enable monitoring after pausing.',
|
|
369
385
|
inputSchema: {
|
|
370
|
-
monitorID: z.number().int().nonnegative().describe('The ID of the monitor to resume')
|
|
386
|
+
monitorID: z.coerce.number().int().nonnegative().describe('The ID of the monitor to resume')
|
|
371
387
|
},
|
|
372
388
|
outputSchema: {
|
|
373
389
|
ok: z.boolean(),
|
|
@@ -395,6 +411,724 @@ export async function createServer(config) {
|
|
|
395
411
|
throw new McpError(ErrorCode.InternalError, `Failed to resume monitor: ${errorMessage}`);
|
|
396
412
|
}
|
|
397
413
|
});
|
|
414
|
+
// ─── Monitor write tools ──────────────────────────────────────────────────
|
|
415
|
+
server.registerTool('createMonitor', {
|
|
416
|
+
title: 'Create Monitor',
|
|
417
|
+
description: 'Creates a new monitor in Uptime Kuma. Requires at minimum a name and type. Use listMonitorTypes to see supported types. For HTTP monitors include url; for TCP/port monitors include hostname and port.',
|
|
418
|
+
inputSchema: {
|
|
419
|
+
name: z.string().describe('Display name for the monitor'),
|
|
420
|
+
type: z.string().describe('Monitor type (e.g. http, port, ping, dns, push, keyword). Use listMonitorTypes for all options.'),
|
|
421
|
+
url: z.string().optional().describe('URL to monitor (required for http/keyword/json-query types)'),
|
|
422
|
+
hostname: z.string().optional().describe('Hostname to monitor (required for port/ping/dns types)'),
|
|
423
|
+
port: z.coerce.number().optional().describe('Port number (required for port/tcp types)'),
|
|
424
|
+
interval: z.coerce.number().optional().describe('Check interval in seconds (default: 60)'),
|
|
425
|
+
retryInterval: z.coerce.number().optional().describe('Retry interval in seconds when monitor is down (default: 60)'),
|
|
426
|
+
maxretries: z.coerce.number().optional().describe('Max retries before marking as down (default: 0)'),
|
|
427
|
+
notificationIDList: z.record(z.string(), z.boolean()).optional().describe('Map of notification IDs to enable (e.g. {"1": true, "3": true})'),
|
|
428
|
+
tags: z.array(z.object({
|
|
429
|
+
name: z.string(),
|
|
430
|
+
value: z.string().optional(),
|
|
431
|
+
color: z.string().optional(),
|
|
432
|
+
})).optional().describe('Tags to assign to the monitor'),
|
|
433
|
+
keyword: z.string().optional().describe('Keyword to search for (keyword monitor type)'),
|
|
434
|
+
invertKeyword: z.boolean().optional().describe('Invert keyword match'),
|
|
435
|
+
method: z.string().optional().describe('HTTP method (GET, POST, etc.) for http type'),
|
|
436
|
+
body: z.string().optional().describe('HTTP request body'),
|
|
437
|
+
headers: z.string().optional().describe('HTTP headers as JSON string'),
|
|
438
|
+
accepted_statuscodes: z.array(z.string()).optional().describe('Accepted HTTP status codes (e.g. ["200-299"])'),
|
|
439
|
+
ignoreTls: z.boolean().optional().describe('Ignore TLS/SSL errors'),
|
|
440
|
+
maxredirects: z.coerce.number().optional().describe('Max HTTP redirects (default: 10)'),
|
|
441
|
+
upsideDown: z.boolean().optional().describe('Invert status — treat up as down'),
|
|
442
|
+
parent: z.coerce.number().nullable().optional().describe('Parent group monitor ID'),
|
|
443
|
+
docker_container: z.string().optional().describe('Docker container name (required for docker type)'),
|
|
444
|
+
docker_host: z.coerce.number().optional().describe('Docker host ID (required for docker type). Use listDockerHosts to find available IDs.'),
|
|
445
|
+
},
|
|
446
|
+
outputSchema: {
|
|
447
|
+
ok: z.boolean(),
|
|
448
|
+
monitorID: z.number().optional(),
|
|
449
|
+
msg: z.string().optional(),
|
|
450
|
+
},
|
|
451
|
+
}, async (input) => {
|
|
452
|
+
if (!isAuthenticated) {
|
|
453
|
+
throw new McpError(ErrorCode.InternalError, 'Not authenticated with Uptime Kuma');
|
|
454
|
+
}
|
|
455
|
+
try {
|
|
456
|
+
const defaults = {
|
|
457
|
+
notificationIDList: {},
|
|
458
|
+
accepted_statuscodes: ['200-299'],
|
|
459
|
+
conditions: [],
|
|
460
|
+
retryInterval: 60,
|
|
461
|
+
};
|
|
462
|
+
const monitorData = {
|
|
463
|
+
...defaults,
|
|
464
|
+
...input,
|
|
465
|
+
};
|
|
466
|
+
const response = await client.createMonitor(monitorData);
|
|
467
|
+
return {
|
|
468
|
+
content: [{ type: 'text', text: response.msg || `Monitor created with ID ${response.monitorID}` }],
|
|
469
|
+
structuredContent: { ok: response.ok, monitorID: response.monitorID, msg: response.msg },
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
catch (error) {
|
|
473
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
474
|
+
throw new McpError(ErrorCode.InternalError, `Failed to create monitor: ${errorMessage}`);
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
server.registerTool('updateMonitor', {
|
|
478
|
+
title: 'Update Monitor',
|
|
479
|
+
description: 'Updates an existing monitor configuration. You must include the monitorID. Only the fields you provide will be changed (the server merges your changes with the existing config). Use getMonitor first to get the current config.',
|
|
480
|
+
inputSchema: {
|
|
481
|
+
monitorID: z.coerce.number().int().nonnegative().describe('The ID of the monitor to update'),
|
|
482
|
+
name: z.string().optional().describe('Display name'),
|
|
483
|
+
url: z.string().optional().describe('URL to monitor'),
|
|
484
|
+
hostname: z.string().optional().describe('Hostname'),
|
|
485
|
+
port: z.coerce.number().optional().describe('Port number'),
|
|
486
|
+
interval: z.coerce.number().optional().describe('Check interval in seconds'),
|
|
487
|
+
retryInterval: z.coerce.number().optional().describe('Retry interval in seconds'),
|
|
488
|
+
maxretries: z.coerce.number().optional().describe('Max retries before marking as down'),
|
|
489
|
+
notificationIDList: z.record(z.string(), z.boolean()).optional().describe('Notification ID map'),
|
|
490
|
+
tags: z.array(z.object({
|
|
491
|
+
name: z.string(),
|
|
492
|
+
value: z.string().optional(),
|
|
493
|
+
color: z.string().optional(),
|
|
494
|
+
})).optional().describe('Tags to assign'),
|
|
495
|
+
keyword: z.string().optional().describe('Keyword to search for'),
|
|
496
|
+
invertKeyword: z.boolean().optional().describe('Invert keyword match'),
|
|
497
|
+
method: z.string().optional().describe('HTTP method'),
|
|
498
|
+
body: z.string().optional().describe('HTTP request body'),
|
|
499
|
+
headers: z.string().optional().describe('HTTP headers as JSON string'),
|
|
500
|
+
accepted_statuscodes: z.array(z.string()).optional().describe('Accepted HTTP status codes'),
|
|
501
|
+
ignoreTls: z.boolean().optional().describe('Ignore TLS/SSL errors'),
|
|
502
|
+
maxredirects: z.coerce.number().optional().describe('Max HTTP redirects'),
|
|
503
|
+
upsideDown: z.boolean().optional().describe('Invert status'),
|
|
504
|
+
active: z.boolean().optional().describe('Whether the monitor is active'),
|
|
505
|
+
docker_container: z.string().optional().describe('Docker container name (required for docker type)'),
|
|
506
|
+
docker_host: z.coerce.number().optional().describe('Docker host ID (required for docker type). Use listDockerHosts to find available IDs.'),
|
|
507
|
+
},
|
|
508
|
+
outputSchema: {
|
|
509
|
+
ok: z.boolean(),
|
|
510
|
+
monitorID: z.number().optional(),
|
|
511
|
+
msg: z.string().optional(),
|
|
512
|
+
},
|
|
513
|
+
}, async ({ monitorID, ...rest }) => {
|
|
514
|
+
if (!isAuthenticated) {
|
|
515
|
+
throw new McpError(ErrorCode.InternalError, 'Not authenticated with Uptime Kuma');
|
|
516
|
+
}
|
|
517
|
+
try {
|
|
518
|
+
const existing = client.getMonitor(monitorID, true);
|
|
519
|
+
if (!existing) {
|
|
520
|
+
throw new Error(`Monitor ${monitorID} not found`);
|
|
521
|
+
}
|
|
522
|
+
// Strip undefined values so existing config is preserved for omitted fields
|
|
523
|
+
const defined = Object.fromEntries(Object.entries(rest).filter(([, v]) => v !== undefined));
|
|
524
|
+
const merged = { ...existing, ...defined, id: monitorID };
|
|
525
|
+
// Ensure retryInterval is valid — Kuma rejects values < 1 on edit even if
|
|
526
|
+
// it stored 0 during creation (pre-existing monitors or older defaults)
|
|
527
|
+
if (!merged.retryInterval || merged.retryInterval < 1) {
|
|
528
|
+
merged.retryInterval = merged.interval || 60;
|
|
529
|
+
}
|
|
530
|
+
const response = await client.updateMonitor(merged);
|
|
531
|
+
return {
|
|
532
|
+
content: [{ type: 'text', text: response.msg || `Monitor ${monitorID} updated successfully` }],
|
|
533
|
+
structuredContent: { ok: response.ok, monitorID: response.monitorID, msg: response.msg },
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
catch (error) {
|
|
537
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
538
|
+
throw new McpError(ErrorCode.InternalError, `Failed to update monitor: ${errorMessage}`);
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
server.registerTool('deleteMonitor', {
|
|
542
|
+
title: 'Delete Monitor',
|
|
543
|
+
description: 'Permanently deletes a monitor and all its heartbeat history. This action cannot be undone.',
|
|
544
|
+
inputSchema: {
|
|
545
|
+
monitorID: z.coerce.number().int().nonnegative().describe('The ID of the monitor to delete'),
|
|
546
|
+
},
|
|
547
|
+
outputSchema: {
|
|
548
|
+
ok: z.boolean(),
|
|
549
|
+
msg: z.string().optional(),
|
|
550
|
+
},
|
|
551
|
+
}, async ({ monitorID }) => {
|
|
552
|
+
if (!isAuthenticated) {
|
|
553
|
+
throw new McpError(ErrorCode.InternalError, 'Not authenticated with Uptime Kuma');
|
|
554
|
+
}
|
|
555
|
+
try {
|
|
556
|
+
const response = await client.deleteMonitor(monitorID);
|
|
557
|
+
return {
|
|
558
|
+
content: [{ type: 'text', text: response.msg || `Monitor ${monitorID} deleted successfully` }],
|
|
559
|
+
structuredContent: { ok: response.ok, msg: response.msg },
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
catch (error) {
|
|
563
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
564
|
+
throw new McpError(ErrorCode.InternalError, `Failed to delete monitor: ${errorMessage}`);
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
// ─── Notification tools ───────────────────────────────────────────────────
|
|
568
|
+
server.registerTool('listNotifications', {
|
|
569
|
+
title: 'List Notifications',
|
|
570
|
+
description: 'Returns all configured notification channels (Slack, ntfy, Discord, email, webhooks, etc.).',
|
|
571
|
+
inputSchema: {},
|
|
572
|
+
outputSchema: {
|
|
573
|
+
notifications: z.array(NotificationSchema).describe('Array of notification channel configurations'),
|
|
574
|
+
count: z.number(),
|
|
575
|
+
},
|
|
576
|
+
}, async () => {
|
|
577
|
+
if (!isAuthenticated) {
|
|
578
|
+
throw new McpError(ErrorCode.InternalError, 'Not authenticated with Uptime Kuma');
|
|
579
|
+
}
|
|
580
|
+
try {
|
|
581
|
+
const notifications = client.getNotificationList();
|
|
582
|
+
return {
|
|
583
|
+
content: [{ type: 'text', text: JSON.stringify(notifications, null, 2) }],
|
|
584
|
+
structuredContent: { notifications, count: notifications.length },
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
589
|
+
throw new McpError(ErrorCode.InternalError, `Failed to list notifications: ${errorMessage}`);
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
server.registerTool('addNotification', {
|
|
593
|
+
title: 'Add Notification',
|
|
594
|
+
description: 'Creates a new notification channel. The configuration fields depend on the notification type (e.g., for slack: webhookURL; for ntfy: ntfyTopic, ntfyServerUrl; for discord: discordWebhookUrl).',
|
|
595
|
+
inputSchema: {
|
|
596
|
+
name: z.string().describe('Human-readable name for this notification channel'),
|
|
597
|
+
type: z.string().describe('Notification type (e.g. slack, ntfy, discord, telegram, webhook, smtp)'),
|
|
598
|
+
isDefault: z.boolean().optional().describe('Enable by default for new monitors'),
|
|
599
|
+
applyExisting: z.boolean().optional().describe('Apply this notification to all existing monitors now'),
|
|
600
|
+
config: z.record(z.string(), z.unknown()).describe('Type-specific configuration fields (e.g. webhookURL for slack, ntfyTopic for ntfy)'),
|
|
601
|
+
},
|
|
602
|
+
outputSchema: {
|
|
603
|
+
ok: z.boolean(),
|
|
604
|
+
id: z.number().optional(),
|
|
605
|
+
msg: z.string().optional(),
|
|
606
|
+
},
|
|
607
|
+
}, async ({ name, type, isDefault, applyExisting, config }) => {
|
|
608
|
+
if (!isAuthenticated) {
|
|
609
|
+
throw new McpError(ErrorCode.InternalError, 'Not authenticated with Uptime Kuma');
|
|
610
|
+
}
|
|
611
|
+
try {
|
|
612
|
+
const notification = { name, type, isDefault, applyExisting, ...config };
|
|
613
|
+
const response = await client.addNotification(notification);
|
|
614
|
+
return {
|
|
615
|
+
content: [{ type: 'text', text: response.msg || `Notification created with ID ${response.id}` }],
|
|
616
|
+
structuredContent: { ok: response.ok, id: response.id, msg: response.msg },
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
catch (error) {
|
|
620
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
621
|
+
throw new McpError(ErrorCode.InternalError, `Failed to add notification: ${errorMessage}`);
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
server.registerTool('updateNotification', {
|
|
625
|
+
title: 'Update Notification',
|
|
626
|
+
description: 'Updates an existing notification channel. Use listNotifications to find the notification ID.',
|
|
627
|
+
inputSchema: {
|
|
628
|
+
notificationID: z.coerce.number().int().nonnegative().describe('The ID of the notification to update'),
|
|
629
|
+
name: z.string().optional().describe('Human-readable name'),
|
|
630
|
+
type: z.string().optional().describe('Notification type'),
|
|
631
|
+
isDefault: z.boolean().optional().describe('Enable by default for new monitors'),
|
|
632
|
+
applyExisting: z.boolean().optional().describe('Apply to all existing monitors now'),
|
|
633
|
+
config: z.record(z.string(), z.unknown()).optional().describe('Type-specific configuration fields to update'),
|
|
634
|
+
},
|
|
635
|
+
outputSchema: {
|
|
636
|
+
ok: z.boolean(),
|
|
637
|
+
id: z.number().optional(),
|
|
638
|
+
msg: z.string().optional(),
|
|
639
|
+
},
|
|
640
|
+
}, async ({ notificationID, name, type, isDefault, applyExisting, config }) => {
|
|
641
|
+
if (!isAuthenticated) {
|
|
642
|
+
throw new McpError(ErrorCode.InternalError, 'Not authenticated with Uptime Kuma');
|
|
643
|
+
}
|
|
644
|
+
try {
|
|
645
|
+
const notification = { ...config };
|
|
646
|
+
if (name !== undefined)
|
|
647
|
+
notification['name'] = name;
|
|
648
|
+
if (type !== undefined)
|
|
649
|
+
notification['type'] = type;
|
|
650
|
+
if (isDefault !== undefined)
|
|
651
|
+
notification['isDefault'] = isDefault;
|
|
652
|
+
if (applyExisting !== undefined)
|
|
653
|
+
notification['applyExisting'] = applyExisting;
|
|
654
|
+
const response = await client.addNotification(notification, notificationID);
|
|
655
|
+
return {
|
|
656
|
+
content: [{ type: 'text', text: response.msg || `Notification ${notificationID} updated` }],
|
|
657
|
+
structuredContent: { ok: response.ok, id: response.id, msg: response.msg },
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
catch (error) {
|
|
661
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
662
|
+
throw new McpError(ErrorCode.InternalError, `Failed to update notification: ${errorMessage}`);
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
server.registerTool('deleteNotification', {
|
|
666
|
+
title: 'Delete Notification',
|
|
667
|
+
description: 'Permanently deletes a notification channel. Monitors that used this channel will no longer send alerts through it.',
|
|
668
|
+
inputSchema: {
|
|
669
|
+
notificationID: z.coerce.number().int().nonnegative().describe('The ID of the notification to delete'),
|
|
670
|
+
},
|
|
671
|
+
outputSchema: {
|
|
672
|
+
ok: z.boolean(),
|
|
673
|
+
msg: z.string().optional(),
|
|
674
|
+
},
|
|
675
|
+
}, async ({ notificationID }) => {
|
|
676
|
+
if (!isAuthenticated) {
|
|
677
|
+
throw new McpError(ErrorCode.InternalError, 'Not authenticated with Uptime Kuma');
|
|
678
|
+
}
|
|
679
|
+
try {
|
|
680
|
+
const response = await client.deleteNotification(notificationID);
|
|
681
|
+
return {
|
|
682
|
+
content: [{ type: 'text', text: response.msg || `Notification ${notificationID} deleted` }],
|
|
683
|
+
structuredContent: { ok: response.ok, msg: response.msg },
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
catch (error) {
|
|
687
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
688
|
+
throw new McpError(ErrorCode.InternalError, `Failed to delete notification: ${errorMessage}`);
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
// ─── Docker host tools ───────────────────────────────────────────────────
|
|
692
|
+
server.registerTool('listDockerHosts', {
|
|
693
|
+
title: 'List Docker Hosts',
|
|
694
|
+
description: 'Returns all docker daemon connections configured in Uptime Kuma. These are referenced by docker container monitors via docker_host.',
|
|
695
|
+
inputSchema: {},
|
|
696
|
+
outputSchema: {
|
|
697
|
+
dockerHosts: z.array(DockerHostSchema).describe('Array of docker host configurations'),
|
|
698
|
+
count: z.number(),
|
|
699
|
+
},
|
|
700
|
+
}, async () => {
|
|
701
|
+
if (!isAuthenticated) {
|
|
702
|
+
throw new McpError(ErrorCode.InternalError, 'Not authenticated with Uptime Kuma');
|
|
703
|
+
}
|
|
704
|
+
try {
|
|
705
|
+
const dockerHosts = client.getDockerHostList();
|
|
706
|
+
return {
|
|
707
|
+
content: [{ type: 'text', text: JSON.stringify(dockerHosts, null, 2) }],
|
|
708
|
+
structuredContent: { dockerHosts, count: dockerHosts.length },
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
catch (error) {
|
|
712
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
713
|
+
throw new McpError(ErrorCode.InternalError, `Failed to list docker hosts: ${errorMessage}`);
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
server.registerTool('addDockerHost', {
|
|
717
|
+
title: 'Add Docker Host',
|
|
718
|
+
description: 'Creates a new docker daemon connection. For a unix socket use dockerType="socket" and dockerDaemon="/var/run/docker.sock". For a TCP proxy (e.g. tecnativa/docker-socket-proxy) use dockerType="tcp" and dockerDaemon="http://host:2375". Consider calling testDockerHost first to verify reachability.',
|
|
719
|
+
inputSchema: {
|
|
720
|
+
name: z.string().describe('Human-readable name for this docker host'),
|
|
721
|
+
dockerType: z.enum(['socket', 'tcp']).describe('"socket" for a unix socket path, "tcp" for an HTTP/HTTPS URL'),
|
|
722
|
+
dockerDaemon: z.string().describe('Unix socket path (e.g. /var/run/docker.sock) when dockerType=socket, or TCP URL (e.g. http://docker-proxy:2375) when dockerType=tcp'),
|
|
723
|
+
},
|
|
724
|
+
outputSchema: {
|
|
725
|
+
ok: z.boolean(),
|
|
726
|
+
id: z.number().optional(),
|
|
727
|
+
msg: z.string().optional(),
|
|
728
|
+
},
|
|
729
|
+
}, async ({ name, dockerType, dockerDaemon }) => {
|
|
730
|
+
if (!isAuthenticated) {
|
|
731
|
+
throw new McpError(ErrorCode.InternalError, 'Not authenticated with Uptime Kuma');
|
|
732
|
+
}
|
|
733
|
+
try {
|
|
734
|
+
const response = await client.addDockerHost({ name, dockerType, dockerDaemon });
|
|
735
|
+
return {
|
|
736
|
+
content: [{ type: 'text', text: response.msg || `Docker host created with ID ${response.id}` }],
|
|
737
|
+
structuredContent: { ok: response.ok, id: response.id, msg: response.msg },
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
catch (error) {
|
|
741
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
742
|
+
throw new McpError(ErrorCode.InternalError, `Failed to add docker host: ${errorMessage}`);
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
server.registerTool('updateDockerHost', {
|
|
746
|
+
title: 'Update Docker Host',
|
|
747
|
+
description: 'Updates an existing docker daemon connection. Use listDockerHosts to find the docker host ID. Only the fields you pass are changed — the others are preserved.',
|
|
748
|
+
inputSchema: {
|
|
749
|
+
dockerHostID: z.coerce.number().int().nonnegative().describe('The ID of the docker host to update'),
|
|
750
|
+
name: z.string().optional().describe('New human-readable name'),
|
|
751
|
+
dockerType: z.enum(['socket', 'tcp']).optional().describe('New connection type'),
|
|
752
|
+
dockerDaemon: z.string().optional().describe('New socket path or TCP URL'),
|
|
753
|
+
},
|
|
754
|
+
outputSchema: {
|
|
755
|
+
ok: z.boolean(),
|
|
756
|
+
id: z.number().optional(),
|
|
757
|
+
msg: z.string().optional(),
|
|
758
|
+
},
|
|
759
|
+
}, async ({ dockerHostID, name, dockerType, dockerDaemon }) => {
|
|
760
|
+
if (!isAuthenticated) {
|
|
761
|
+
throw new McpError(ErrorCode.InternalError, 'Not authenticated with Uptime Kuma');
|
|
762
|
+
}
|
|
763
|
+
try {
|
|
764
|
+
// Merge new values onto the current record so callers can omit unchanged fields.
|
|
765
|
+
// Uptime Kuma's addDockerHost handler overwrites every column it receives, so we
|
|
766
|
+
// need to send the full set to avoid clobbering existing values with undefined.
|
|
767
|
+
const existing = client.getDockerHostList().find(h => h.id === dockerHostID);
|
|
768
|
+
if (!existing) {
|
|
769
|
+
throw new Error(`Docker host ${dockerHostID} not found — call listDockerHosts to see available IDs`);
|
|
770
|
+
}
|
|
771
|
+
const merged = {
|
|
772
|
+
name: name ?? existing.name,
|
|
773
|
+
dockerType: dockerType ?? existing.dockerType,
|
|
774
|
+
dockerDaemon: dockerDaemon ?? existing.dockerDaemon,
|
|
775
|
+
};
|
|
776
|
+
const response = await client.addDockerHost(merged, dockerHostID);
|
|
777
|
+
return {
|
|
778
|
+
content: [{ type: 'text', text: response.msg || `Docker host ${dockerHostID} updated` }],
|
|
779
|
+
structuredContent: { ok: response.ok, id: response.id, msg: response.msg },
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
catch (error) {
|
|
783
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
784
|
+
throw new McpError(ErrorCode.InternalError, `Failed to update docker host: ${errorMessage}`);
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
server.registerTool('deleteDockerHost', {
|
|
788
|
+
title: 'Delete Docker Host',
|
|
789
|
+
description: 'Permanently deletes a docker daemon connection. Any monitors referencing it will have their docker_host cleared by Uptime Kuma (the monitors themselves are not deleted).',
|
|
790
|
+
inputSchema: {
|
|
791
|
+
dockerHostID: z.coerce.number().int().nonnegative().describe('The ID of the docker host to delete'),
|
|
792
|
+
},
|
|
793
|
+
outputSchema: {
|
|
794
|
+
ok: z.boolean(),
|
|
795
|
+
msg: z.string().optional(),
|
|
796
|
+
},
|
|
797
|
+
}, async ({ dockerHostID }) => {
|
|
798
|
+
if (!isAuthenticated) {
|
|
799
|
+
throw new McpError(ErrorCode.InternalError, 'Not authenticated with Uptime Kuma');
|
|
800
|
+
}
|
|
801
|
+
try {
|
|
802
|
+
const response = await client.deleteDockerHost(dockerHostID);
|
|
803
|
+
return {
|
|
804
|
+
content: [{ type: 'text', text: response.msg || `Docker host ${dockerHostID} deleted` }],
|
|
805
|
+
structuredContent: { ok: response.ok, msg: response.msg },
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
catch (error) {
|
|
809
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
810
|
+
throw new McpError(ErrorCode.InternalError, `Failed to delete docker host: ${errorMessage}`);
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
server.registerTool('testDockerHost', {
|
|
814
|
+
title: 'Test Docker Host',
|
|
815
|
+
description: 'Tests connectivity to a docker daemon without persisting it. On success the message includes the number of containers. Use this before addDockerHost to avoid saving a broken configuration.',
|
|
816
|
+
inputSchema: {
|
|
817
|
+
name: z.string().describe('Display name (used only in the test request)'),
|
|
818
|
+
dockerType: z.enum(['socket', 'tcp']).describe('"socket" for a unix socket path, "tcp" for an HTTP/HTTPS URL'),
|
|
819
|
+
dockerDaemon: z.string().describe('Unix socket path or TCP URL to probe'),
|
|
820
|
+
},
|
|
821
|
+
outputSchema: {
|
|
822
|
+
ok: z.boolean(),
|
|
823
|
+
msg: z.string().optional(),
|
|
824
|
+
},
|
|
825
|
+
}, async ({ name, dockerType, dockerDaemon }) => {
|
|
826
|
+
if (!isAuthenticated) {
|
|
827
|
+
throw new McpError(ErrorCode.InternalError, 'Not authenticated with Uptime Kuma');
|
|
828
|
+
}
|
|
829
|
+
try {
|
|
830
|
+
const response = await client.testDockerHost({ name, dockerType, dockerDaemon });
|
|
831
|
+
return {
|
|
832
|
+
content: [{ type: 'text', text: response.msg || (response.ok ? 'Docker host reachable' : 'Docker host unreachable') }],
|
|
833
|
+
structuredContent: { ok: response.ok, msg: response.msg },
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
catch (error) {
|
|
837
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
838
|
+
throw new McpError(ErrorCode.InternalError, `Failed to test docker host: ${errorMessage}`);
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
// ─── Tag tools ───────────────────────────────────────────────────────────
|
|
842
|
+
server.registerTool('listTags', {
|
|
843
|
+
title: 'List Tags',
|
|
844
|
+
description: 'Returns all tags defined in Uptime Kuma (name, color, and ID).',
|
|
845
|
+
inputSchema: {},
|
|
846
|
+
outputSchema: {
|
|
847
|
+
tags: z.array(z.object({
|
|
848
|
+
id: z.number(),
|
|
849
|
+
name: z.string(),
|
|
850
|
+
color: z.string(),
|
|
851
|
+
})).describe('Array of tags'),
|
|
852
|
+
count: z.number(),
|
|
853
|
+
},
|
|
854
|
+
}, async () => {
|
|
855
|
+
if (!isAuthenticated) {
|
|
856
|
+
throw new McpError(ErrorCode.InternalError, 'Not authenticated with Uptime Kuma');
|
|
857
|
+
}
|
|
858
|
+
try {
|
|
859
|
+
const tags = client.getTagList();
|
|
860
|
+
return {
|
|
861
|
+
content: [{ type: 'text', text: JSON.stringify(tags, null, 2) }],
|
|
862
|
+
structuredContent: { tags, count: tags.length },
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
catch (error) {
|
|
866
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
867
|
+
throw new McpError(ErrorCode.InternalError, `Failed to list tags: ${errorMessage}`);
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
server.registerTool('addTag', {
|
|
871
|
+
title: 'Add Tag',
|
|
872
|
+
description: 'Creates a new tag that can be assigned to monitors.',
|
|
873
|
+
inputSchema: {
|
|
874
|
+
name: z.string().describe('Tag name'),
|
|
875
|
+
color: z.string().describe('Tag color as a hex string (e.g. "#ff0000") or CSS color name'),
|
|
876
|
+
},
|
|
877
|
+
outputSchema: {
|
|
878
|
+
ok: z.boolean(),
|
|
879
|
+
tag: z.object({ id: z.number(), name: z.string(), color: z.string() }).optional(),
|
|
880
|
+
msg: z.string().optional(),
|
|
881
|
+
},
|
|
882
|
+
}, async ({ name, color }) => {
|
|
883
|
+
if (!isAuthenticated) {
|
|
884
|
+
throw new McpError(ErrorCode.InternalError, 'Not authenticated with Uptime Kuma');
|
|
885
|
+
}
|
|
886
|
+
try {
|
|
887
|
+
const response = await client.addTag(name, color);
|
|
888
|
+
return {
|
|
889
|
+
content: [{ type: 'text', text: `Tag "${name}" created with ID ${response.tag?.id}` }],
|
|
890
|
+
structuredContent: { ok: response.ok, tag: response.tag, msg: response.msg },
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
catch (error) {
|
|
894
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
895
|
+
throw new McpError(ErrorCode.InternalError, `Failed to add tag: ${errorMessage}`);
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
server.registerTool('deleteTag', {
|
|
899
|
+
title: 'Delete Tag',
|
|
900
|
+
description: 'Permanently deletes a tag. It will be removed from all monitors that use it. Use listTags to find the tag ID.',
|
|
901
|
+
inputSchema: {
|
|
902
|
+
tagID: z.coerce.number().int().nonnegative().describe('The ID of the tag to delete'),
|
|
903
|
+
},
|
|
904
|
+
outputSchema: {
|
|
905
|
+
ok: z.boolean(),
|
|
906
|
+
msg: z.string().optional(),
|
|
907
|
+
},
|
|
908
|
+
}, async ({ tagID }) => {
|
|
909
|
+
if (!isAuthenticated) {
|
|
910
|
+
throw new McpError(ErrorCode.InternalError, 'Not authenticated with Uptime Kuma');
|
|
911
|
+
}
|
|
912
|
+
try {
|
|
913
|
+
const response = await client.deleteTag(tagID);
|
|
914
|
+
return {
|
|
915
|
+
content: [{ type: 'text', text: response.msg || `Tag ${tagID} deleted` }],
|
|
916
|
+
structuredContent: { ok: response.ok, msg: response.msg },
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
catch (error) {
|
|
920
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
921
|
+
throw new McpError(ErrorCode.InternalError, `Failed to delete tag: ${errorMessage}`);
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
// ─── Maintenance tools ────────────────────────────────────────────────────
|
|
925
|
+
server.registerTool('getMaintenanceWindows', {
|
|
926
|
+
title: 'Get Maintenance Windows',
|
|
927
|
+
description: 'Returns all scheduled maintenance windows defined in Uptime Kuma.',
|
|
928
|
+
inputSchema: {},
|
|
929
|
+
outputSchema: {
|
|
930
|
+
maintenanceWindows: z.array(MaintenanceSchema).describe('Array of maintenance windows'),
|
|
931
|
+
count: z.number(),
|
|
932
|
+
},
|
|
933
|
+
}, async () => {
|
|
934
|
+
if (!isAuthenticated) {
|
|
935
|
+
throw new McpError(ErrorCode.InternalError, 'Not authenticated with Uptime Kuma');
|
|
936
|
+
}
|
|
937
|
+
try {
|
|
938
|
+
const maintenanceWindows = client.getMaintenanceList();
|
|
939
|
+
return {
|
|
940
|
+
content: [{ type: 'text', text: JSON.stringify(maintenanceWindows, null, 2) }],
|
|
941
|
+
structuredContent: { maintenanceWindows, count: maintenanceWindows.length },
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
catch (error) {
|
|
945
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
946
|
+
throw new McpError(ErrorCode.InternalError, `Failed to get maintenance windows: ${errorMessage}`);
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
server.registerTool('createMaintenance', {
|
|
950
|
+
title: 'Create Maintenance',
|
|
951
|
+
description: 'Schedules a new maintenance window. During maintenance, affected monitors are suppressed and show MAINTENANCE status instead of DOWN.',
|
|
952
|
+
inputSchema: {
|
|
953
|
+
title: z.string().describe('Title of the maintenance window'),
|
|
954
|
+
description: z.string().default('').describe('Description or reason for the maintenance'),
|
|
955
|
+
strategy: z.enum(['single', 'recurring-interval', 'recurring-weekday', 'recurring-day-of-month', 'manual'])
|
|
956
|
+
.describe('Scheduling strategy: single=one-time, recurring-interval=every N days, recurring-weekday=specific weekdays, recurring-day-of-month=specific dates, manual=manually activated'),
|
|
957
|
+
active: z.boolean().optional().describe('Whether the window is active (default: true)'),
|
|
958
|
+
timezone: z.string().optional().describe('Timezone (e.g. "America/New_York", "UTC"). Defaults to server timezone.'),
|
|
959
|
+
dateRange: z.array(z.string()).optional().describe('Date range as [startISO, endISO] (required for single strategy)'),
|
|
960
|
+
timeRange: z.array(z.object({ hours: z.coerce.number(), minutes: z.coerce.number() })).optional()
|
|
961
|
+
.describe('Start and end time within the day as [{hours, minutes}, {hours, minutes}]'),
|
|
962
|
+
weekdays: z.array(z.coerce.number().int().min(0).max(6)).optional()
|
|
963
|
+
.describe('Days of week (0=Sunday … 6=Saturday) for recurring-weekday strategy'),
|
|
964
|
+
daysOfMonth: z.array(z.coerce.number().int().min(1).max(31)).optional()
|
|
965
|
+
.describe('Days of month (1-31) for recurring-day-of-month strategy'),
|
|
966
|
+
intervalDay: z.coerce.number().int().positive().optional()
|
|
967
|
+
.describe('Interval in days for recurring-interval strategy'),
|
|
968
|
+
},
|
|
969
|
+
outputSchema: {
|
|
970
|
+
ok: z.boolean(),
|
|
971
|
+
maintenanceID: z.number().optional(),
|
|
972
|
+
msg: z.string().optional(),
|
|
973
|
+
},
|
|
974
|
+
}, async (input) => {
|
|
975
|
+
if (!isAuthenticated) {
|
|
976
|
+
throw new McpError(ErrorCode.InternalError, 'Not authenticated with Uptime Kuma');
|
|
977
|
+
}
|
|
978
|
+
try {
|
|
979
|
+
const response = await client.createMaintenance(input);
|
|
980
|
+
return {
|
|
981
|
+
content: [{ type: 'text', text: response.msg || `Maintenance window created with ID ${response.maintenanceID}` }],
|
|
982
|
+
structuredContent: { ok: response.ok, maintenanceID: response.maintenanceID, msg: response.msg },
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
catch (error) {
|
|
986
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
987
|
+
throw new McpError(ErrorCode.InternalError, `Failed to create maintenance window: ${errorMessage}`);
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
// ─── Status page tools ────────────────────────────────────────────────────
|
|
991
|
+
server.registerTool('listStatusPages', {
|
|
992
|
+
title: 'List Status Pages',
|
|
993
|
+
description: 'Returns all configured status pages with their slug, title, visibility, and custom domain settings.',
|
|
994
|
+
inputSchema: {},
|
|
995
|
+
outputSchema: {
|
|
996
|
+
statusPages: z.array(StatusPageSchema).describe('Array of status page configurations'),
|
|
997
|
+
count: z.number(),
|
|
998
|
+
},
|
|
999
|
+
}, async () => {
|
|
1000
|
+
if (!isAuthenticated) {
|
|
1001
|
+
throw new McpError(ErrorCode.InternalError, 'Not authenticated with Uptime Kuma');
|
|
1002
|
+
}
|
|
1003
|
+
try {
|
|
1004
|
+
const statusPages = client.getStatusPageList();
|
|
1005
|
+
return {
|
|
1006
|
+
content: [{ type: 'text', text: JSON.stringify(statusPages, null, 2) }],
|
|
1007
|
+
structuredContent: { statusPages, count: statusPages.length },
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
catch (error) {
|
|
1011
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
1012
|
+
throw new McpError(ErrorCode.InternalError, `Failed to list status pages: ${errorMessage}`);
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
server.registerTool('getStatusPage', {
|
|
1016
|
+
title: 'Get Status Page',
|
|
1017
|
+
description: 'Returns the full configuration of a status page by slug, including the ordered list of groups, the monitors inside each group, and active incidents. Only works for published status pages (fetches the public `/api/status-page/{slug}` endpoint).',
|
|
1018
|
+
inputSchema: {
|
|
1019
|
+
slug: z.string().describe('The status page slug (the URL-safe identifier)'),
|
|
1020
|
+
},
|
|
1021
|
+
outputSchema: {
|
|
1022
|
+
ok: z.boolean(),
|
|
1023
|
+
config: StatusPageSchema.optional(),
|
|
1024
|
+
publicGroupList: z.array(z.record(z.string(), z.unknown())).optional().describe('Ordered groups with their monitorList'),
|
|
1025
|
+
incidents: z.array(z.record(z.string(), z.unknown())).optional().describe('Active incidents on the status page'),
|
|
1026
|
+
msg: z.string().optional(),
|
|
1027
|
+
},
|
|
1028
|
+
}, async ({ slug }) => {
|
|
1029
|
+
if (!isAuthenticated) {
|
|
1030
|
+
throw new McpError(ErrorCode.InternalError, 'Not authenticated with Uptime Kuma');
|
|
1031
|
+
}
|
|
1032
|
+
try {
|
|
1033
|
+
const response = await client.getStatusPage(slug);
|
|
1034
|
+
return {
|
|
1035
|
+
content: [{ type: 'text', text: JSON.stringify(response, null, 2) }],
|
|
1036
|
+
structuredContent: {
|
|
1037
|
+
ok: response.ok,
|
|
1038
|
+
config: response.config,
|
|
1039
|
+
publicGroupList: response.publicGroupList,
|
|
1040
|
+
incidents: response.incidents,
|
|
1041
|
+
msg: response.msg,
|
|
1042
|
+
},
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
catch (error) {
|
|
1046
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
1047
|
+
throw new McpError(ErrorCode.InternalError, `Failed to get status page: ${errorMessage}`);
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
server.registerTool('createStatusPage', {
|
|
1051
|
+
title: 'Create Status Page',
|
|
1052
|
+
description: 'Creates a new (empty) status page with the given title and slug. After creating, call updateStatusPage to set the description, theme, groups, and monitors. Slug must be lowercase letters, digits, and dashes only.',
|
|
1053
|
+
inputSchema: {
|
|
1054
|
+
title: z.string().describe('Display title of the status page'),
|
|
1055
|
+
slug: z.string().regex(/^[a-z0-9-]+$/).describe('URL slug (lowercase letters, digits, and dashes only)'),
|
|
1056
|
+
},
|
|
1057
|
+
outputSchema: {
|
|
1058
|
+
ok: z.boolean(),
|
|
1059
|
+
msg: z.string().optional(),
|
|
1060
|
+
},
|
|
1061
|
+
}, async ({ title, slug }) => {
|
|
1062
|
+
if (!isAuthenticated) {
|
|
1063
|
+
throw new McpError(ErrorCode.InternalError, 'Not authenticated with Uptime Kuma');
|
|
1064
|
+
}
|
|
1065
|
+
try {
|
|
1066
|
+
const response = await client.createStatusPage(title, slug);
|
|
1067
|
+
return {
|
|
1068
|
+
content: [{ type: 'text', text: response.msg || `Status page ${slug} created` }],
|
|
1069
|
+
structuredContent: { ok: response.ok, msg: response.msg },
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
catch (error) {
|
|
1073
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
1074
|
+
throw new McpError(ErrorCode.InternalError, `Failed to create status page: ${errorMessage}`);
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
server.registerTool('updateStatusPage', {
|
|
1078
|
+
title: 'Update Status Page',
|
|
1079
|
+
description: 'Updates an existing status page. Pass the full config (title, description, theme, published, etc.) and the full publicGroupList — both are replaced wholesale. Each group has a name, weight, and monitorList of [{id}]. Use getStatusPage first to read current state before modifying.',
|
|
1080
|
+
inputSchema: {
|
|
1081
|
+
slug: z.string().describe('The status page slug (immutable identifier)'),
|
|
1082
|
+
config: z.record(z.string(), z.unknown()).describe('Full status page config (title, description, theme, published, showTags, showPoweredBy, domainNameList, customCSS, footerText, icon, etc.)'),
|
|
1083
|
+
publicGroupList: z.array(z.record(z.string(), z.unknown())).optional().describe('Ordered groups. Each: {name, weight, monitorList: [{id}]}. Defaults to empty list.'),
|
|
1084
|
+
imgDataUrl: z.string().optional().describe('Icon as data URL. Omit or pass empty string to keep existing.'),
|
|
1085
|
+
},
|
|
1086
|
+
outputSchema: {
|
|
1087
|
+
ok: z.boolean(),
|
|
1088
|
+
msg: z.string().optional(),
|
|
1089
|
+
},
|
|
1090
|
+
}, async ({ slug, config, publicGroupList, imgDataUrl }) => {
|
|
1091
|
+
if (!isAuthenticated) {
|
|
1092
|
+
throw new McpError(ErrorCode.InternalError, 'Not authenticated with Uptime Kuma');
|
|
1093
|
+
}
|
|
1094
|
+
try {
|
|
1095
|
+
const response = await client.updateStatusPage(slug, config, publicGroupList ?? [], imgDataUrl ?? '');
|
|
1096
|
+
return {
|
|
1097
|
+
content: [{ type: 'text', text: response.msg || `Status page ${slug} updated` }],
|
|
1098
|
+
structuredContent: { ok: response.ok, msg: response.msg },
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
catch (error) {
|
|
1102
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
1103
|
+
throw new McpError(ErrorCode.InternalError, `Failed to update status page: ${errorMessage}`);
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
server.registerTool('deleteStatusPage', {
|
|
1107
|
+
title: 'Delete Status Page',
|
|
1108
|
+
description: 'Permanently deletes a status page by slug. The status page URL will no longer be accessible.',
|
|
1109
|
+
inputSchema: {
|
|
1110
|
+
slug: z.string().describe('The status page slug to delete'),
|
|
1111
|
+
},
|
|
1112
|
+
outputSchema: {
|
|
1113
|
+
ok: z.boolean(),
|
|
1114
|
+
msg: z.string().optional(),
|
|
1115
|
+
},
|
|
1116
|
+
}, async ({ slug }) => {
|
|
1117
|
+
if (!isAuthenticated) {
|
|
1118
|
+
throw new McpError(ErrorCode.InternalError, 'Not authenticated with Uptime Kuma');
|
|
1119
|
+
}
|
|
1120
|
+
try {
|
|
1121
|
+
const response = await client.deleteStatusPage(slug);
|
|
1122
|
+
return {
|
|
1123
|
+
content: [{ type: 'text', text: response.msg || `Status page ${slug} deleted` }],
|
|
1124
|
+
structuredContent: { ok: response.ok, msg: response.msg },
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
catch (error) {
|
|
1128
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
1129
|
+
throw new McpError(ErrorCode.InternalError, `Failed to delete status page: ${errorMessage}`);
|
|
1130
|
+
}
|
|
1131
|
+
});
|
|
398
1132
|
// Clean up on server shutdown
|
|
399
1133
|
process.on('SIGINT', () => {
|
|
400
1134
|
client.disconnect();
|