@visualsentinel/mcp-server 0.1.0 → 0.2.2
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/CHANGELOG.md +46 -0
- package/README.md +54 -7
- package/dist/.metadata_never_index +0 -0
- package/dist/api-client.d.ts +32 -0
- package/dist/api-client.js +79 -0
- package/dist/api-client.js.map +1 -0
- package/dist/index.js +457 -16
- package/dist/index.js.map +1 -1
- package/dist/prompts.d.ts +22 -0
- package/dist/prompts.js +105 -0
- package/dist/prompts.js.map +1 -0
- package/dist/resources.d.ts +18 -0
- package/dist/resources.js +625 -0
- package/dist/resources.js.map +1 -0
- package/dist/tools.d.ts +38 -0
- package/dist/tools.js +546 -0
- package/dist/tools.js.map +1 -0
- package/dist/version.d.ts +4 -0
- package/dist/version.js +8 -0
- package/dist/version.js.map +1 -0
- package/package.json +15 -11
package/dist/index.js
CHANGED
|
@@ -5,7 +5,13 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
6
|
import {
|
|
7
7
|
CallToolRequestSchema,
|
|
8
|
-
|
|
8
|
+
GetPromptRequestSchema,
|
|
9
|
+
ListPromptsRequestSchema,
|
|
10
|
+
ListResourcesRequestSchema,
|
|
11
|
+
ListToolsRequestSchema,
|
|
12
|
+
McpError,
|
|
13
|
+
ErrorCode,
|
|
14
|
+
ReadResourceRequestSchema
|
|
9
15
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
16
|
|
|
11
17
|
// src/api-client.ts
|
|
@@ -39,7 +45,7 @@ var ApiClient = class {
|
|
|
39
45
|
const auth = options.auth ?? true;
|
|
40
46
|
if (auth && !this.hasApiKey()) {
|
|
41
47
|
throw new VsApiError(
|
|
42
|
-
"Visual Sentinel API key
|
|
48
|
+
"This tool needs a Visual Sentinel API key, which is available on the Business and Agency plans. Create one at https://visualsentinel.com/settings/api-keys and set it as VS_API_KEY. The DNS, SSL, speed, website-check and health tools work without a key.",
|
|
43
49
|
401,
|
|
44
50
|
null
|
|
45
51
|
);
|
|
@@ -103,6 +109,14 @@ var TOOLS = [
|
|
|
103
109
|
// -------------------------------------------------------------------------
|
|
104
110
|
{
|
|
105
111
|
name: "vs_health",
|
|
112
|
+
title: "Check Visual Sentinel service health",
|
|
113
|
+
annotations: {
|
|
114
|
+
title: "Check Visual Sentinel service health",
|
|
115
|
+
readOnlyHint: true,
|
|
116
|
+
destructiveHint: false,
|
|
117
|
+
idempotentHint: true,
|
|
118
|
+
openWorldHint: true
|
|
119
|
+
},
|
|
106
120
|
description: "Check whether Visual Sentinel itself is up. Returns the service health status. No authentication required.",
|
|
107
121
|
inputSchema: {
|
|
108
122
|
type: "object",
|
|
@@ -114,6 +128,14 @@ var TOOLS = [
|
|
|
114
128
|
},
|
|
115
129
|
{
|
|
116
130
|
name: "vs_dns_check",
|
|
131
|
+
title: "Look up DNS records",
|
|
132
|
+
annotations: {
|
|
133
|
+
title: "Look up DNS records",
|
|
134
|
+
readOnlyHint: true,
|
|
135
|
+
destructiveHint: false,
|
|
136
|
+
idempotentHint: true,
|
|
137
|
+
openWorldHint: true
|
|
138
|
+
},
|
|
117
139
|
description: "Resolve DNS records (A, AAAA, MX, NS, TXT, CNAME) for a domain using Visual Sentinel's public DNS lookup tool. No authentication required.",
|
|
118
140
|
inputSchema: {
|
|
119
141
|
type: "object",
|
|
@@ -132,31 +154,48 @@ var TOOLS = [
|
|
|
132
154
|
auth: false,
|
|
133
155
|
query: {
|
|
134
156
|
domain: requireString(args, "domain"),
|
|
135
|
-
|
|
157
|
+
// The API reads ?type= (not recordType).
|
|
158
|
+
type: pickString(args, "recordType")
|
|
136
159
|
}
|
|
137
160
|
})
|
|
138
161
|
},
|
|
139
162
|
{
|
|
140
163
|
name: "vs_ssl_check",
|
|
164
|
+
title: "Inspect an SSL certificate",
|
|
165
|
+
annotations: {
|
|
166
|
+
title: "Inspect an SSL certificate",
|
|
167
|
+
readOnlyHint: true,
|
|
168
|
+
destructiveHint: false,
|
|
169
|
+
idempotentHint: true,
|
|
170
|
+
openWorldHint: true
|
|
171
|
+
},
|
|
141
172
|
description: "Inspect a TLS/SSL certificate for a hostname: issuer, subject, validity dates, SAN list, key algorithm, and certificate chain. No authentication required.",
|
|
142
173
|
inputSchema: {
|
|
143
174
|
type: "object",
|
|
144
175
|
properties: {
|
|
145
|
-
|
|
146
|
-
port: { ...INT, description: "TCP port. Default 443." }
|
|
176
|
+
domain: { ...STR, description: "Hostname to inspect, e.g. example.com (without protocol)." }
|
|
147
177
|
},
|
|
148
|
-
required: ["
|
|
178
|
+
required: ["domain"],
|
|
149
179
|
additionalProperties: false
|
|
150
180
|
},
|
|
151
181
|
requiresAuth: false,
|
|
152
182
|
handler: async (args, client) => client.request("GET", "/api/tools/ssl-check", {
|
|
153
183
|
auth: false,
|
|
154
|
-
|
|
184
|
+
// The API reads ?domain=. `host` stays as an input alias for 0.1.x callers.
|
|
185
|
+
query: { domain: pickString(args, "domain") ?? requireString(args, "host") }
|
|
155
186
|
})
|
|
156
187
|
},
|
|
157
188
|
{
|
|
158
189
|
name: "vs_speed_test",
|
|
159
|
-
|
|
190
|
+
title: "Run a one-shot speed test",
|
|
191
|
+
annotations: {
|
|
192
|
+
title: "Run a one-shot speed test",
|
|
193
|
+
readOnlyHint: true,
|
|
194
|
+
destructiveHint: false,
|
|
195
|
+
idempotentHint: true,
|
|
196
|
+
openWorldHint: true
|
|
197
|
+
},
|
|
198
|
+
description: "Run a one-shot Lighthouse lab test on a URL. Returns the performance score (0 to 100) plus Largest Contentful Paint, First Contentful Paint, Cumulative Layout Shift, Time to First Byte, Total Blocking Time and Speed Index (times in milliseconds). No account needed.",
|
|
160
199
|
inputSchema: {
|
|
161
200
|
type: "object",
|
|
162
201
|
properties: {
|
|
@@ -173,6 +212,14 @@ var TOOLS = [
|
|
|
173
212
|
},
|
|
174
213
|
{
|
|
175
214
|
name: "vs_website_check",
|
|
215
|
+
title: "Check a website",
|
|
216
|
+
annotations: {
|
|
217
|
+
title: "Check a website",
|
|
218
|
+
readOnlyHint: true,
|
|
219
|
+
destructiveHint: false,
|
|
220
|
+
idempotentHint: true,
|
|
221
|
+
openWorldHint: true
|
|
222
|
+
},
|
|
176
223
|
description: "Quick health check for a URL: HTTP status, response time, server header, content snippet, and basic SSL state. No authentication required.",
|
|
177
224
|
inputSchema: {
|
|
178
225
|
type: "object",
|
|
@@ -193,6 +240,14 @@ var TOOLS = [
|
|
|
193
240
|
// -------------------------------------------------------------------------
|
|
194
241
|
{
|
|
195
242
|
name: "vs_monitors_list",
|
|
243
|
+
title: "List monitors",
|
|
244
|
+
annotations: {
|
|
245
|
+
title: "List monitors",
|
|
246
|
+
readOnlyHint: true,
|
|
247
|
+
destructiveHint: false,
|
|
248
|
+
idempotentHint: true,
|
|
249
|
+
openWorldHint: false
|
|
250
|
+
},
|
|
196
251
|
description: "List monitors in the authenticated organization. Optional filters narrow by status, type, or paginate.",
|
|
197
252
|
inputSchema: {
|
|
198
253
|
type: "object",
|
|
@@ -219,6 +274,14 @@ var TOOLS = [
|
|
|
219
274
|
},
|
|
220
275
|
{
|
|
221
276
|
name: "vs_monitors_get",
|
|
277
|
+
title: "Get a monitor",
|
|
278
|
+
annotations: {
|
|
279
|
+
title: "Get a monitor",
|
|
280
|
+
readOnlyHint: true,
|
|
281
|
+
destructiveHint: false,
|
|
282
|
+
idempotentHint: true,
|
|
283
|
+
openWorldHint: false
|
|
284
|
+
},
|
|
222
285
|
description: "Fetch a single monitor by id, including its current status, last check, and configuration.",
|
|
223
286
|
inputSchema: {
|
|
224
287
|
type: "object",
|
|
@@ -233,6 +296,14 @@ var TOOLS = [
|
|
|
233
296
|
},
|
|
234
297
|
{
|
|
235
298
|
name: "vs_monitors_create",
|
|
299
|
+
title: "Create a monitor",
|
|
300
|
+
annotations: {
|
|
301
|
+
title: "Create a monitor",
|
|
302
|
+
readOnlyHint: false,
|
|
303
|
+
destructiveHint: false,
|
|
304
|
+
idempotentHint: false,
|
|
305
|
+
openWorldHint: false
|
|
306
|
+
},
|
|
236
307
|
description: "Create a new monitor. Provide at least name, url, and type. Returns the created monitor including its assigned id.",
|
|
237
308
|
inputSchema: {
|
|
238
309
|
type: "object",
|
|
@@ -268,6 +339,14 @@ var TOOLS = [
|
|
|
268
339
|
},
|
|
269
340
|
{
|
|
270
341
|
name: "vs_monitors_check_now",
|
|
342
|
+
title: "Run a monitor check now",
|
|
343
|
+
annotations: {
|
|
344
|
+
title: "Run a monitor check now",
|
|
345
|
+
readOnlyHint: false,
|
|
346
|
+
destructiveHint: false,
|
|
347
|
+
idempotentHint: false,
|
|
348
|
+
openWorldHint: false
|
|
349
|
+
},
|
|
271
350
|
description: "Trigger an immediate check for a monitor (in addition to its scheduled cadence). Returns the freshly-collected check result.",
|
|
272
351
|
inputSchema: {
|
|
273
352
|
type: "object",
|
|
@@ -282,26 +361,127 @@ var TOOLS = [
|
|
|
282
361
|
},
|
|
283
362
|
{
|
|
284
363
|
name: "vs_monitors_uptime",
|
|
285
|
-
|
|
364
|
+
title: "Get monitor uptime",
|
|
365
|
+
annotations: {
|
|
366
|
+
title: "Get monitor uptime",
|
|
367
|
+
readOnlyHint: true,
|
|
368
|
+
destructiveHint: false,
|
|
369
|
+
idempotentHint: true,
|
|
370
|
+
openWorldHint: false
|
|
371
|
+
},
|
|
372
|
+
description: "Fetch the uptime percentage and time-series breakdown for a monitor. Use `window` for a preset window (24h, 7d, 30d, 90d, 365d; default 30d), supply both `from` and `to` (ISO 8601) for a custom date range, or set `all: true` for the full available history.",
|
|
286
373
|
inputSchema: {
|
|
287
374
|
type: "object",
|
|
288
375
|
properties: {
|
|
289
376
|
id: { ...STR, description: "Monitor id." },
|
|
290
377
|
window: {
|
|
291
378
|
...STR,
|
|
292
|
-
description: "
|
|
379
|
+
description: "Preset window: 1h, 6h, 24h, 7d, 30d, 90d, 365d. Default 30d. Ignored when from+to or all are supplied."
|
|
380
|
+
},
|
|
381
|
+
from: {
|
|
382
|
+
...STR,
|
|
383
|
+
description: "ISO 8601 start of a custom range (e.g. 2026-01-01T00:00:00Z). Must be supplied with `to`."
|
|
384
|
+
},
|
|
385
|
+
to: {
|
|
386
|
+
...STR,
|
|
387
|
+
description: "ISO 8601 end of a custom range. Must be supplied with `from`."
|
|
388
|
+
},
|
|
389
|
+
all: {
|
|
390
|
+
type: "boolean",
|
|
391
|
+
description: "Return the full available history from the earliest stored data. Takes precedence over from/to and window."
|
|
293
392
|
}
|
|
294
393
|
},
|
|
295
394
|
required: ["id"],
|
|
296
395
|
additionalProperties: false
|
|
297
396
|
},
|
|
298
397
|
requiresAuth: true,
|
|
299
|
-
handler: async (args, client) =>
|
|
300
|
-
|
|
301
|
-
|
|
398
|
+
handler: async (args, client) => {
|
|
399
|
+
const from = pickString(args, "from");
|
|
400
|
+
const to = pickString(args, "to");
|
|
401
|
+
const query = args["all"] === true ? { all: true } : from && to ? { from, to } : { period: pickString(args, "window") ?? "30d" };
|
|
402
|
+
return client.request(
|
|
403
|
+
"GET",
|
|
404
|
+
`/api/monitors/${encodeURIComponent(requireString(args, "id"))}/uptime`,
|
|
405
|
+
{ query }
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
name: "vs_monitors_metrics_bulk",
|
|
411
|
+
title: "Get bulk monitor metrics",
|
|
412
|
+
annotations: {
|
|
413
|
+
title: "Get bulk monitor metrics",
|
|
414
|
+
readOnlyHint: true,
|
|
415
|
+
destructiveHint: false,
|
|
416
|
+
idempotentHint: true,
|
|
417
|
+
openWorldHint: false
|
|
418
|
+
},
|
|
419
|
+
description: "Fetch uptime or response-time metrics for up to 100 monitors in a single request. Use `window` for a preset window (1h, 6h, 24h, 7d, 30d, 90d, 365d; default 30d), supply both `from` and `to` (ISO 8601) for a custom date range, or set `all: true` for the full available history. Returns a `monitors` object keyed by monitor id.",
|
|
420
|
+
inputSchema: {
|
|
421
|
+
type: "object",
|
|
422
|
+
properties: {
|
|
423
|
+
monitorIds: {
|
|
424
|
+
type: "array",
|
|
425
|
+
items: { type: "string" },
|
|
426
|
+
description: "List of monitor IDs to fetch metrics for (max 100)."
|
|
427
|
+
},
|
|
428
|
+
metric: {
|
|
429
|
+
...STR,
|
|
430
|
+
description: 'Metric to return: "uptime" or "response-time".'
|
|
431
|
+
},
|
|
432
|
+
window: {
|
|
433
|
+
...STR,
|
|
434
|
+
description: "Preset window: 1h, 6h, 24h, 7d, 30d, 90d, 365d. Default 30d. Ignored when from+to or all are supplied."
|
|
435
|
+
},
|
|
436
|
+
from: {
|
|
437
|
+
...STR,
|
|
438
|
+
description: "ISO 8601 start of a custom range (e.g. 2026-01-01T00:00:00Z). Must be supplied with `to`."
|
|
439
|
+
},
|
|
440
|
+
to: {
|
|
441
|
+
...STR,
|
|
442
|
+
description: "ISO 8601 end of a custom range. Must be supplied with `from`."
|
|
443
|
+
},
|
|
444
|
+
all: {
|
|
445
|
+
type: "boolean",
|
|
446
|
+
description: "Return the full available history from the earliest stored data. Takes precedence over from/to and window."
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
required: ["monitorIds", "metric"],
|
|
450
|
+
additionalProperties: false
|
|
451
|
+
},
|
|
452
|
+
requiresAuth: true,
|
|
453
|
+
handler: async (args, client) => {
|
|
454
|
+
const monitorIds = args["monitorIds"];
|
|
455
|
+
if (!Array.isArray(monitorIds) || monitorIds.length === 0) {
|
|
456
|
+
throw new Error('Argument "monitorIds" must be a non-empty array.');
|
|
457
|
+
}
|
|
458
|
+
const from = pickString(args, "from");
|
|
459
|
+
const to = pickString(args, "to");
|
|
460
|
+
const body = {
|
|
461
|
+
monitorIds,
|
|
462
|
+
metric: requireString(args, "metric")
|
|
463
|
+
};
|
|
464
|
+
if (args["all"] === true) {
|
|
465
|
+
body["all"] = true;
|
|
466
|
+
} else if (from && to) {
|
|
467
|
+
body["from"] = from;
|
|
468
|
+
body["to"] = to;
|
|
469
|
+
} else {
|
|
470
|
+
body["period"] = pickString(args, "window") ?? "30d";
|
|
471
|
+
}
|
|
472
|
+
return client.request("POST", "/api/monitors/metrics", { body });
|
|
473
|
+
}
|
|
302
474
|
},
|
|
303
475
|
{
|
|
304
476
|
name: "vs_incidents_list",
|
|
477
|
+
title: "List incidents",
|
|
478
|
+
annotations: {
|
|
479
|
+
title: "List incidents",
|
|
480
|
+
readOnlyHint: true,
|
|
481
|
+
destructiveHint: false,
|
|
482
|
+
idempotentHint: true,
|
|
483
|
+
openWorldHint: false
|
|
484
|
+
},
|
|
305
485
|
description: "List incidents for the authenticated organization. Filter by status (OPEN/RESOLVED), monitor id, or paginate.",
|
|
306
486
|
inputSchema: {
|
|
307
487
|
type: "object",
|
|
@@ -325,6 +505,14 @@ var TOOLS = [
|
|
|
325
505
|
},
|
|
326
506
|
{
|
|
327
507
|
name: "vs_incidents_get",
|
|
508
|
+
title: "Get an incident",
|
|
509
|
+
annotations: {
|
|
510
|
+
title: "Get an incident",
|
|
511
|
+
readOnlyHint: true,
|
|
512
|
+
destructiveHint: false,
|
|
513
|
+
idempotentHint: true,
|
|
514
|
+
openWorldHint: false
|
|
515
|
+
},
|
|
328
516
|
description: "Fetch a single incident with its full check history and root-cause hints.",
|
|
329
517
|
inputSchema: {
|
|
330
518
|
type: "object",
|
|
@@ -339,6 +527,14 @@ var TOOLS = [
|
|
|
339
527
|
},
|
|
340
528
|
{
|
|
341
529
|
name: "vs_alerts_list",
|
|
530
|
+
title: "List alerts",
|
|
531
|
+
annotations: {
|
|
532
|
+
title: "List alerts",
|
|
533
|
+
readOnlyHint: true,
|
|
534
|
+
destructiveHint: false,
|
|
535
|
+
idempotentHint: true,
|
|
536
|
+
openWorldHint: false
|
|
537
|
+
},
|
|
342
538
|
description: "List alerts for the authenticated organization. Filter by status (UNACKNOWLEDGED/ACKNOWLEDGED/RESOLVED) or paginate.",
|
|
343
539
|
inputSchema: {
|
|
344
540
|
type: "object",
|
|
@@ -360,7 +556,15 @@ var TOOLS = [
|
|
|
360
556
|
},
|
|
361
557
|
{
|
|
362
558
|
name: "vs_alerts_acknowledge",
|
|
363
|
-
|
|
559
|
+
title: "Acknowledge an alert",
|
|
560
|
+
annotations: {
|
|
561
|
+
title: "Acknowledge an alert",
|
|
562
|
+
readOnlyHint: false,
|
|
563
|
+
destructiveHint: false,
|
|
564
|
+
idempotentHint: true,
|
|
565
|
+
openWorldHint: false
|
|
566
|
+
},
|
|
567
|
+
description: "Acknowledge an alert by id. The acknowledgement is recorded against the account that authorised this connection.",
|
|
364
568
|
inputSchema: {
|
|
365
569
|
type: "object",
|
|
366
570
|
properties: {
|
|
@@ -377,6 +581,14 @@ var TOOLS = [
|
|
|
377
581
|
},
|
|
378
582
|
{
|
|
379
583
|
name: "vs_status_pages_list",
|
|
584
|
+
title: "List status pages",
|
|
585
|
+
annotations: {
|
|
586
|
+
title: "List status pages",
|
|
587
|
+
readOnlyHint: true,
|
|
588
|
+
destructiveHint: false,
|
|
589
|
+
idempotentHint: true,
|
|
590
|
+
openWorldHint: false
|
|
591
|
+
},
|
|
380
592
|
description: "List public-facing status pages owned by the authenticated organization.",
|
|
381
593
|
inputSchema: {
|
|
382
594
|
type: "object",
|
|
@@ -388,6 +600,14 @@ var TOOLS = [
|
|
|
388
600
|
},
|
|
389
601
|
{
|
|
390
602
|
name: "vs_servers_list",
|
|
603
|
+
title: "List servers",
|
|
604
|
+
annotations: {
|
|
605
|
+
title: "List servers",
|
|
606
|
+
readOnlyHint: true,
|
|
607
|
+
destructiveHint: false,
|
|
608
|
+
idempotentHint: true,
|
|
609
|
+
openWorldHint: false
|
|
610
|
+
},
|
|
391
611
|
description: "List servers (origins) registered in the authenticated organization.",
|
|
392
612
|
inputSchema: {
|
|
393
613
|
type: "object",
|
|
@@ -407,13 +627,198 @@ function findTool(name) {
|
|
|
407
627
|
return TOOLS.find((t) => t.name === name);
|
|
408
628
|
}
|
|
409
629
|
|
|
630
|
+
// src/prompts.ts
|
|
631
|
+
function need(args, key) {
|
|
632
|
+
const v = args[key]?.trim();
|
|
633
|
+
if (!v) throw new Error(`Argument "${key}" is required.`);
|
|
634
|
+
return v;
|
|
635
|
+
}
|
|
636
|
+
function hostOf(value) {
|
|
637
|
+
return value.replace(/^https?:\/\//i, "").replace(/\/.*$/, "");
|
|
638
|
+
}
|
|
639
|
+
var PROMPTS = [
|
|
640
|
+
{
|
|
641
|
+
name: "check_website_health",
|
|
642
|
+
title: "Check a website's health",
|
|
643
|
+
description: "Run uptime, SSL, DNS and speed checks on one site and summarise what needs attention. No account or API key needed.",
|
|
644
|
+
arguments: [
|
|
645
|
+
{ name: "url", description: "Site to check, e.g. https://example.com", required: true }
|
|
646
|
+
],
|
|
647
|
+
build: (args) => {
|
|
648
|
+
const url = need(args, "url");
|
|
649
|
+
const host = hostOf(url);
|
|
650
|
+
return [
|
|
651
|
+
`Check the health of ${url} with the Visual Sentinel tools and report back in plain language.`,
|
|
652
|
+
"",
|
|
653
|
+
`1. vs_website_check with url "${url}": HTTP status, response time, server header.`,
|
|
654
|
+
`2. vs_ssl_check with domain "${host}": certificate validity, expiry date and chain.`,
|
|
655
|
+
`3. vs_dns_check with domain "${host}": A, AAAA, MX, NS and TXT records.`,
|
|
656
|
+
`4. vs_speed_test with url "${url}": time to first byte, total load time, transfer size, redirects.`,
|
|
657
|
+
"",
|
|
658
|
+
"Then give: a one-line verdict, a short list of anything that looks wrong (non-200 status, certificate close to expiry, missing or odd DNS records, slow response, long redirect chain), and the one fix to do first. Say which checks succeeded and which returned an error. Do not guess at anything a tool did not return."
|
|
659
|
+
].join("\n");
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
name: "investigate_broken_site",
|
|
664
|
+
title: "Investigate why my site looks broken",
|
|
665
|
+
description: "Work out why a site is down, slow or looking wrong by checking each layer in order. With an API key it also reads open incidents and recent alerts.",
|
|
666
|
+
arguments: [
|
|
667
|
+
{ name: "url", description: "The affected page, e.g. https://example.com/pricing", required: true },
|
|
668
|
+
{ name: "symptom", description: 'What you see, e.g. "blank page", "images missing", "very slow"', required: false }
|
|
669
|
+
],
|
|
670
|
+
build: (args) => {
|
|
671
|
+
const url = need(args, "url");
|
|
672
|
+
const host = hostOf(url);
|
|
673
|
+
const symptom = args.symptom?.trim();
|
|
674
|
+
return [
|
|
675
|
+
`${symptom ? `The page ${url} shows this problem: ${symptom}.` : `Something looks wrong on ${url}.`} Find the most likely layer at fault, using the Visual Sentinel tools in this order and stopping to report as soon as you find a clear cause:`,
|
|
676
|
+
"",
|
|
677
|
+
`1. DNS: vs_dns_check for "${host}". Do the records resolve, and do they point where they should?`,
|
|
678
|
+
`2. TLS: vs_ssl_check for "${host}". Is the certificate valid, unexpired and issued for this hostname?`,
|
|
679
|
+
`3. Server response: vs_website_check for "${url}". Status code, response time, content snippet.`,
|
|
680
|
+
`4. Speed: vs_speed_test for "${url}". Is time to first byte or load time unusually high, and is there a redirect chain?`,
|
|
681
|
+
"5. If a Visual Sentinel API key is configured: vs_monitors_list to find a monitor for this site, then vs_incidents_list and vs_alerts_list for it, and vs_monitors_uptime for the last 24 hours. If no key is set, say that account data was not checked.",
|
|
682
|
+
"",
|
|
683
|
+
"A page can return HTTP 200 and still look broken (missing styles, empty content, a failed script). If every check above is healthy, say so and suggest comparing the page against an earlier screenshot, which is what Visual Sentinel visual monitoring does.",
|
|
684
|
+
"Report: the evidence from each step, the most likely cause, and the next action."
|
|
685
|
+
].join("\n");
|
|
686
|
+
}
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
name: "pre_launch_checklist",
|
|
690
|
+
title: "Pre-launch checklist for a site",
|
|
691
|
+
description: "Check a new or migrated site before it goes live: DNS, certificate, response and speed, then a short list of fixes. No API key needed.",
|
|
692
|
+
arguments: [
|
|
693
|
+
{ name: "domain", description: "The domain being launched, e.g. example.com", required: true }
|
|
694
|
+
],
|
|
695
|
+
build: (args) => {
|
|
696
|
+
const domain = hostOf(need(args, "domain"));
|
|
697
|
+
return [
|
|
698
|
+
`Run a pre-launch check on ${domain} with the Visual Sentinel tools and return a checklist with Pass, Warn or Fail for each line.`,
|
|
699
|
+
"",
|
|
700
|
+
`- DNS: vs_dns_check for "${domain}" and for "www.${domain}". Both resolve, and the records point to the intended host. Check MX and TXT (SPF) records if the domain sends email.`,
|
|
701
|
+
`- Certificate: vs_ssl_check for "${domain}" and for "www.${domain}". Valid, issued for the hostname, and more than 30 days from expiry.`,
|
|
702
|
+
`- Response: vs_website_check for "https://${domain}" and "https://www.${domain}". Both return 200 or one cleanly redirects to the other.`,
|
|
703
|
+
`- Speed: vs_speed_test for "https://${domain}". Report time to first byte, total load time and redirect count.`,
|
|
704
|
+
"",
|
|
705
|
+
"Finish with the Fail items first, each with a concrete fix, then the Warn items. Add one line suggesting monitors to create after launch (uptime on the home page, certificate expiry, DNS records), and say that creating them needs a Visual Sentinel account with an API key. Do not create anything unless asked."
|
|
706
|
+
].join("\n");
|
|
707
|
+
}
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
name: "triage_open_incidents",
|
|
711
|
+
title: "Triage open incidents",
|
|
712
|
+
description: "Review open incidents and unacknowledged alerts on your account and rank what to look at first. Needs a Visual Sentinel API key (Business and Agency plans).",
|
|
713
|
+
arguments: [],
|
|
714
|
+
build: () => [
|
|
715
|
+
"Review my Visual Sentinel account and tell me what needs attention first.",
|
|
716
|
+
"",
|
|
717
|
+
"1. vs_incidents_list with status OPEN, then vs_incidents_get for each one (up to 5, most recent first).",
|
|
718
|
+
"2. vs_alerts_list with status UNACKNOWLEDGED.",
|
|
719
|
+
"3. For each affected monitor, vs_monitors_get and vs_monitors_uptime for the last 24 hours.",
|
|
720
|
+
"",
|
|
721
|
+
'Rank the incidents by customer impact (production sites and checkout pages before staging), group alerts that belong to the same incident, and give one suggested next step for each. If the tools return an "API key required" error, tell me that account tools need an API key from the Business or Agency plan, and stop. Ask before acknowledging any alert.'
|
|
722
|
+
].join("\n")
|
|
723
|
+
}
|
|
724
|
+
];
|
|
725
|
+
function findPrompt(name) {
|
|
726
|
+
return PROMPTS.find((p) => p.name === name);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// src/version.ts
|
|
730
|
+
var SERVER_VERSION = "0.2.2";
|
|
731
|
+
|
|
732
|
+
// src/resources.ts
|
|
733
|
+
var PLANS_AS_OF = "2026-10-16";
|
|
734
|
+
var PLANS = `# Visual Sentinel plans and limits (as of ${PLANS_AS_OF})
|
|
735
|
+
|
|
736
|
+
Prices and the current list are always at https://visualsentinel.com/pricing. There is no free plan for new accounts: a new workspace gets a first check of its site, then a 7-day trial (card required) or a paid plan.
|
|
737
|
+
|
|
738
|
+
| Plan | Monitors | Fastest uptime check | Visual pages | Status pages | API keys and MCP account tools |
|
|
739
|
+
|---|---|---|---|---|---|
|
|
740
|
+
| Solo | 10 | every 5 minutes | 1 site, every 30 minutes | 1 | no |
|
|
741
|
+
| Starter | 15 | every 5 minutes | 5, every 15 minutes | 1 | no |
|
|
742
|
+
| Business | 60 | every 1 minute | 60, every 15 minutes | 5 | yes |
|
|
743
|
+
| Agency | 300 | every 1 minute | 300, every 15 minutes | 50 | yes |
|
|
744
|
+
|
|
745
|
+
- The five public tools (vs_health, vs_dns_check, vs_ssl_check, vs_speed_test, vs_website_check) need no account.
|
|
746
|
+
- The twelve account tools need an API key, which is available on the Business and Agency plans. Create one at https://visualsentinel.com/settings/api-keys.
|
|
747
|
+
- Checks run from Europe and the US. Alert channels on every plan: email, Slack, Discord, Microsoft Teams, Telegram, PagerDuty, Opsgenie, webhooks, WhatsApp, Prometheus, and push notifications in the mobile app.
|
|
748
|
+
`;
|
|
749
|
+
var HELP = `# Using the Visual Sentinel MCP server
|
|
750
|
+
|
|
751
|
+
Visual Sentinel catches pages that look broken even when the site is up. It watches uptime, performance, SSL certificates, DNS, visual changes and content changes.
|
|
752
|
+
|
|
753
|
+
## Start without an account
|
|
754
|
+
Ask for a DNS lookup, an SSL certificate check, a speed test or a website check on any public site. These five tools need no account or key.
|
|
755
|
+
|
|
756
|
+
## Connect your account
|
|
757
|
+
1. Get an API key (Business and Agency plans): https://visualsentinel.com/settings/api-keys
|
|
758
|
+
2. Set it as VS_API_KEY (or VISUALSENTINEL_API_KEY) in your MCP client's environment for this server.
|
|
759
|
+
3. Restart the client. Monitor, uptime, incident, alert, status-page and server tools now work.
|
|
760
|
+
|
|
761
|
+
## Ready-made prompts
|
|
762
|
+
check_website_health, investigate_broken_site, pre_launch_checklist and triage_open_incidents.
|
|
763
|
+
|
|
764
|
+
## Links
|
|
765
|
+
- Docs and install: https://visualsentinel.com/mcp
|
|
766
|
+
- API reference: https://visualsentinel.com/docs
|
|
767
|
+
- Pricing: https://visualsentinel.com/pricing
|
|
768
|
+
- Source: https://github.com/VisualSentinel/mcp-server
|
|
769
|
+
- Support: https://visualsentinel.com/contact
|
|
770
|
+
`;
|
|
771
|
+
function toolsReference() {
|
|
772
|
+
const rows = TOOLS.map(
|
|
773
|
+
(t) => `| ${t.name} | ${t.requiresAuth ? "API key (Business and Agency)" : "none"} | ${t.annotations.readOnlyHint ? "read-only" : "changes data"} | ${(t.description.split(". ")[0] ?? "").replace(/\.$/, "")} |`
|
|
774
|
+
);
|
|
775
|
+
return [
|
|
776
|
+
"# Visual Sentinel tools",
|
|
777
|
+
"",
|
|
778
|
+
"| Tool | Needs | Type | What it does |",
|
|
779
|
+
"|---|---|---|---|",
|
|
780
|
+
...rows,
|
|
781
|
+
""
|
|
782
|
+
].join("\n");
|
|
783
|
+
}
|
|
784
|
+
var RESOURCES = [
|
|
785
|
+
{
|
|
786
|
+
uri: "visualsentinel://plans",
|
|
787
|
+
name: "plans",
|
|
788
|
+
title: "Plans and limits",
|
|
789
|
+
description: "Monitor counts, check frequencies and which plans include API keys and the MCP account tools.",
|
|
790
|
+
mimeType: "text/markdown",
|
|
791
|
+
read: () => PLANS
|
|
792
|
+
},
|
|
793
|
+
{
|
|
794
|
+
uri: "visualsentinel://help",
|
|
795
|
+
name: "help",
|
|
796
|
+
title: "Help and setup",
|
|
797
|
+
description: "How to use the server without an account, how to add an API key, and where the docs are.",
|
|
798
|
+
mimeType: "text/markdown",
|
|
799
|
+
read: () => HELP
|
|
800
|
+
},
|
|
801
|
+
{
|
|
802
|
+
uri: "visualsentinel://tools",
|
|
803
|
+
name: "tools",
|
|
804
|
+
title: "Tool reference",
|
|
805
|
+
description: "Every tool, whether it needs an API key, and whether it is read-only.",
|
|
806
|
+
mimeType: "text/markdown",
|
|
807
|
+
read: toolsReference
|
|
808
|
+
}
|
|
809
|
+
];
|
|
810
|
+
function findResource(uri) {
|
|
811
|
+
return RESOURCES.find((r) => r.uri === uri);
|
|
812
|
+
}
|
|
813
|
+
|
|
410
814
|
// src/index.ts
|
|
411
815
|
var SERVER_NAME = "visualsentinel";
|
|
412
|
-
var SERVER_VERSION = "0.1.0";
|
|
413
816
|
function toMcpToolDescriptor(tool) {
|
|
414
817
|
return {
|
|
415
818
|
name: tool.name,
|
|
819
|
+
title: tool.title,
|
|
416
820
|
description: tool.description,
|
|
821
|
+
annotations: tool.annotations,
|
|
417
822
|
inputSchema: tool.inputSchema
|
|
418
823
|
};
|
|
419
824
|
}
|
|
@@ -421,11 +826,47 @@ async function main() {
|
|
|
421
826
|
const client = new ApiClient();
|
|
422
827
|
const server = new Server(
|
|
423
828
|
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
424
|
-
{ capabilities: { tools: {} } }
|
|
829
|
+
{ capabilities: { tools: {}, prompts: {}, resources: {} } }
|
|
425
830
|
);
|
|
426
831
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
427
832
|
tools: TOOLS.map(toMcpToolDescriptor)
|
|
428
833
|
}));
|
|
834
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
835
|
+
prompts: PROMPTS.map((p) => ({
|
|
836
|
+
name: p.name,
|
|
837
|
+
title: p.title,
|
|
838
|
+
description: p.description,
|
|
839
|
+
arguments: p.arguments
|
|
840
|
+
}))
|
|
841
|
+
}));
|
|
842
|
+
server.setRequestHandler(GetPromptRequestSchema, async (req) => {
|
|
843
|
+
const prompt = findPrompt(req.params.name);
|
|
844
|
+
if (!prompt) throw new McpError(ErrorCode.InvalidParams, `Unknown prompt: ${req.params.name}`);
|
|
845
|
+
let text;
|
|
846
|
+
try {
|
|
847
|
+
text = prompt.build(req.params.arguments ?? {});
|
|
848
|
+
} catch (err) {
|
|
849
|
+
throw new McpError(ErrorCode.InvalidParams, err instanceof Error ? err.message : String(err));
|
|
850
|
+
}
|
|
851
|
+
return {
|
|
852
|
+
description: prompt.description,
|
|
853
|
+
messages: [{ role: "user", content: { type: "text", text } }]
|
|
854
|
+
};
|
|
855
|
+
});
|
|
856
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
857
|
+
resources: RESOURCES.map((r) => ({
|
|
858
|
+
uri: r.uri,
|
|
859
|
+
name: r.name,
|
|
860
|
+
title: r.title,
|
|
861
|
+
description: r.description,
|
|
862
|
+
mimeType: r.mimeType
|
|
863
|
+
}))
|
|
864
|
+
}));
|
|
865
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
|
|
866
|
+
const resource = findResource(req.params.uri);
|
|
867
|
+
if (!resource) throw new McpError(ErrorCode.InvalidParams, `Unknown resource: ${req.params.uri}`);
|
|
868
|
+
return { contents: [{ uri: resource.uri, mimeType: resource.mimeType, text: resource.read() }] };
|
|
869
|
+
});
|
|
429
870
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
430
871
|
const tool = findTool(req.params.name);
|
|
431
872
|
if (!tool) {
|