@visualsentinel/mcp-server 0.2.1 → 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 CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to `@visualsentinel/mcp-server`. Format follows [Keep a Changelog](https://keepachangelog.com/) and the package uses [Semantic Versioning](https://semver.org/).
4
4
 
5
+ ## [0.2.2] - 2026-11-06
6
+
7
+ ### Added
8
+
9
+ * Four MCP prompts: `check_website_health`, `investigate_broken_site`, `pre_launch_checklist`, `triage_open_incidents`.
10
+ * Three read-only MCP resources: `visualsentinel://plans`, `visualsentinel://help`, `visualsentinel://tools`. The server now advertises the `prompts` and `resources` capabilities.
11
+ * LobeHub badge and homepage link in the README (the LobeHub claim route).
12
+ * `mcpName` is now `com.visualsentinel/mcp-server` (the registry namespace is verified through a DNS record on visualsentinel.com).
13
+
14
+ ### Changed
15
+
16
+ * `package.json` `homepage` is now https://visualsentinel.com/mcp, so npm and its mirrors link to the product page instead of this repository.
17
+ * Account tools called without an API key now explain that API keys are available on the Business and Agency plans and that the five public tools work without one.
18
+ * `SERVER_VERSION` lives in `src/version.ts`; a test keeps it equal to `package.json`.
19
+
5
20
  ## [0.2.1] - 2026-10-16
6
21
 
7
22
  ### Fixed
package/README.md CHANGED
@@ -1,9 +1,12 @@
1
1
  # Visual Sentinel MCP Server
2
2
 
3
+ [![MCP Badge](https://lobehub.com/badge/mcp/visualsentinel-mcp-server)](https://lobehub.com/mcp/visualsentinel-mcp-server)
4
+
3
5
  Model Context Protocol server for [Visual Sentinel](https://visualsentinel.com). Visual Sentinel catches pages that look broken even when the site is up. It monitors six layers: uptime, performance, SSL, DNS, visual regression, and content change.
4
6
 
5
7
  Connect Claude Desktop, Claude Code, Cursor, Cline, Continue, or any MCP-compatible client so an AI assistant can run DNS, SSL and speed checks on any site with no account. With a Visual Sentinel account and an API key (Business and Agency plans) it can also list and create monitors, fetch incident history, and acknowledge alerts on your behalf.
6
8
 
9
+ * **Homepage and docs:** https://visualsentinel.com/mcp
7
10
  * **npm:** `@visualsentinel/mcp-server`
8
11
  * **OpenAPI spec:** https://github.com/VisualSentinel/openapi
9
12
  * **API key (Business and Agency plans):** https://visualsentinel.com/settings/api-keys
@@ -40,6 +43,25 @@ Connect Claude Desktop, Claude Code, Cursor, Cline, Continue, or any MCP-compati
40
43
  | `vs_status_pages_list` | `GET /api/status-pages` | List your public status pages |
41
44
  | `vs_servers_list` | `GET /api/servers` | List registered origin servers |
42
45
 
46
+ ## Prompts and resources
47
+
48
+ Besides the 17 tools, the server offers four ready-made prompts and three read-only resources. None of them need an API key except the last prompt.
49
+
50
+ | Prompt | What it does |
51
+ |---|---|
52
+ | `check_website_health` | Runs the website, SSL, DNS and speed checks on one site and summarises what needs attention. |
53
+ | `investigate_broken_site` | Works through DNS, certificate, response and speed to find the layer at fault; reads incidents and alerts too when an API key is set. |
54
+ | `pre_launch_checklist` | Pass, Warn or Fail checklist for a new or migrated domain. |
55
+ | `triage_open_incidents` | Ranks open incidents and unacknowledged alerts (needs an API key). |
56
+
57
+ | Resource | Contents |
58
+ |---|---|
59
+ | `visualsentinel://plans` | Plan limits, and which plans include API keys and the account tools. |
60
+ | `visualsentinel://help` | Setup without an account, adding an API key, links. |
61
+ | `visualsentinel://tools` | Every tool, whether it needs a key, and whether it changes data. |
62
+
63
+ Without an API key the server still starts and lists everything. The five public tools work; each account tool returns a clear message that it needs an API key (Business and Agency plans) instead of failing.
64
+
43
65
  ## Installation
44
66
 
45
67
  ### Claude Desktop
File without changes
@@ -31,7 +31,7 @@ var ApiClient = class {
31
31
  const auth = options.auth ?? true;
32
32
  if (auth && !this.hasApiKey()) {
33
33
  throw new VsApiError(
34
- "Visual Sentinel API key required. Set VS_API_KEY (https://visualsentinel.com/settings/api-keys).",
34
+ "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.",
35
35
  401,
36
36
  null
37
37
  );
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/api-client.ts"],"sourcesContent":["/**\n * Tiny HTTP client for the Visual Sentinel public API.\n *\n * Reads the API key from `VS_API_KEY` (or `VISUALSENTINEL_API_KEY`) once at\n * construction time. All requests are routed at `https://visualsentinel.com`\n * unless `VS_API_BASE` overrides it (useful for testing against a local\n * dev instance).\n */\n\nconst DEFAULT_BASE = 'https://visualsentinel.com';\n\nexport interface ApiClientOptions {\n apiKey?: string | undefined;\n baseUrl?: string | undefined;\n userAgent?: string;\n}\n\nexport class VsApiError extends Error {\n constructor(\n message: string,\n public readonly status: number,\n public readonly body: unknown,\n ) {\n super(message);\n this.name = 'VsApiError';\n }\n}\n\nexport class ApiClient {\n private readonly apiKey: string | undefined;\n private readonly baseUrl: string;\n private readonly userAgent: string;\n\n constructor(opts: ApiClientOptions = {}) {\n this.apiKey =\n opts.apiKey ??\n process.env.VS_API_KEY ??\n process.env.VISUALSENTINEL_API_KEY ??\n undefined;\n this.baseUrl = (opts.baseUrl ?? process.env.VS_API_BASE ?? DEFAULT_BASE).replace(\n /\\/+$/,\n '',\n );\n this.userAgent = opts.userAgent ?? '@visualsentinel/mcp-server';\n }\n\n hasApiKey(): boolean {\n return typeof this.apiKey === 'string' && this.apiKey.length > 0;\n }\n\n async request<T = unknown>(\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',\n path: string,\n options: { query?: Record<string, string | number | boolean | undefined>; body?: unknown; auth?: boolean } = {},\n ): Promise<T> {\n const auth = options.auth ?? true;\n if (auth && !this.hasApiKey()) {\n throw new VsApiError(\n 'Visual Sentinel API key required. Set VS_API_KEY (https://visualsentinel.com/settings/api-keys).',\n 401,\n null,\n );\n }\n\n const url = new URL(path.startsWith('/') ? path : `/${path}`, this.baseUrl);\n if (options.query) {\n for (const [k, v] of Object.entries(options.query)) {\n if (v !== undefined && v !== null) url.searchParams.set(k, String(v));\n }\n }\n\n const headers: Record<string, string> = {\n Accept: 'application/json',\n 'User-Agent': this.userAgent,\n };\n if (auth && this.apiKey) headers['X-API-Key'] = this.apiKey;\n if (options.body !== undefined) headers['Content-Type'] = 'application/json';\n\n const res = await fetch(url, {\n method,\n headers,\n body: options.body !== undefined ? JSON.stringify(options.body) : undefined,\n });\n\n const text = await res.text();\n let parsed: unknown = null;\n if (text.length > 0) {\n try {\n parsed = JSON.parse(text);\n } catch {\n parsed = text;\n }\n }\n\n if (!res.ok) {\n throw new VsApiError(\n `Visual Sentinel API ${method} ${path} returned ${res.status}`,\n res.status,\n parsed,\n );\n }\n\n return parsed as T;\n }\n}\n"],"mappings":";;;AASA,IAAM,eAAe;AAQd,IAAM,aAAN,cAAyB,MAAM;AAAA,EACpC,YACE,SACgB,QACA,MAChB;AACA,UAAM,OAAO;AAHG;AACA;AAGhB,SAAK,OAAO;AAAA,EACd;AAAA,EALkB;AAAA,EACA;AAKpB;AAEO,IAAM,YAAN,MAAgB;AAAA,EACJ;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,OAAyB,CAAC,GAAG;AACvC,SAAK,SACH,KAAK,UACL,QAAQ,IAAI,cACZ,QAAQ,IAAI,0BACZ;AACF,SAAK,WAAW,KAAK,WAAW,QAAQ,IAAI,eAAe,cAAc;AAAA,MACvE;AAAA,MACA;AAAA,IACF;AACA,SAAK,YAAY,KAAK,aAAa;AAAA,EACrC;AAAA,EAEA,YAAqB;AACnB,WAAO,OAAO,KAAK,WAAW,YAAY,KAAK,OAAO,SAAS;AAAA,EACjE;AAAA,EAEA,MAAM,QACJ,QACA,MACA,UAA6G,CAAC,GAClG;AACZ,UAAM,OAAO,QAAQ,QAAQ;AAC7B,QAAI,QAAQ,CAAC,KAAK,UAAU,GAAG;AAC7B,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,UAAM,MAAM,IAAI,IAAI,KAAK,WAAW,GAAG,IAAI,OAAO,IAAI,IAAI,IAAI,KAAK,OAAO;AAC1E,QAAI,QAAQ,OAAO;AACjB,iBAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,QAAQ,KAAK,GAAG;AAClD,YAAI,MAAM,UAAa,MAAM,KAAM,KAAI,aAAa,IAAI,GAAG,OAAO,CAAC,CAAC;AAAA,MACtE;AAAA,IACF;AAEA,UAAM,UAAkC;AAAA,MACtC,QAAQ;AAAA,MACR,cAAc,KAAK;AAAA,IACrB;AACA,QAAI,QAAQ,KAAK,OAAQ,SAAQ,WAAW,IAAI,KAAK;AACrD,QAAI,QAAQ,SAAS,OAAW,SAAQ,cAAc,IAAI;AAE1D,UAAM,MAAM,MAAM,MAAM,KAAK;AAAA,MAC3B;AAAA,MACA;AAAA,MACA,MAAM,QAAQ,SAAS,SAAY,KAAK,UAAU,QAAQ,IAAI,IAAI;AAAA,IACpE,CAAC;AAED,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,QAAI,SAAkB;AACtB,QAAI,KAAK,SAAS,GAAG;AACnB,UAAI;AACF,iBAAS,KAAK,MAAM,IAAI;AAAA,MAC1B,QAAQ;AACN,iBAAS;AAAA,MACX;AAAA,IACF;AAEA,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,IAAI;AAAA,QACR,uBAAuB,MAAM,IAAI,IAAI,aAAa,IAAI,MAAM;AAAA,QAC5D,IAAI;AAAA,QACJ;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/api-client.ts"],"sourcesContent":["/**\n * Tiny HTTP client for the Visual Sentinel public API.\n *\n * Reads the API key from `VS_API_KEY` (or `VISUALSENTINEL_API_KEY`) once at\n * construction time. All requests are routed at `https://visualsentinel.com`\n * unless `VS_API_BASE` overrides it (useful for testing against a local\n * dev instance).\n */\n\nconst DEFAULT_BASE = 'https://visualsentinel.com';\n\nexport interface ApiClientOptions {\n apiKey?: string | undefined;\n baseUrl?: string | undefined;\n userAgent?: string;\n}\n\nexport class VsApiError extends Error {\n constructor(\n message: string,\n public readonly status: number,\n public readonly body: unknown,\n ) {\n super(message);\n this.name = 'VsApiError';\n }\n}\n\nexport class ApiClient {\n private readonly apiKey: string | undefined;\n private readonly baseUrl: string;\n private readonly userAgent: string;\n\n constructor(opts: ApiClientOptions = {}) {\n this.apiKey =\n opts.apiKey ??\n process.env.VS_API_KEY ??\n process.env.VISUALSENTINEL_API_KEY ??\n undefined;\n this.baseUrl = (opts.baseUrl ?? process.env.VS_API_BASE ?? DEFAULT_BASE).replace(\n /\\/+$/,\n '',\n );\n this.userAgent = opts.userAgent ?? '@visualsentinel/mcp-server';\n }\n\n hasApiKey(): boolean {\n return typeof this.apiKey === 'string' && this.apiKey.length > 0;\n }\n\n async request<T = unknown>(\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',\n path: string,\n options: { query?: Record<string, string | number | boolean | undefined>; body?: unknown; auth?: boolean } = {},\n ): Promise<T> {\n const auth = options.auth ?? true;\n if (auth && !this.hasApiKey()) {\n throw new VsApiError(\n '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.',\n 401,\n null,\n );\n }\n\n const url = new URL(path.startsWith('/') ? path : `/${path}`, this.baseUrl);\n if (options.query) {\n for (const [k, v] of Object.entries(options.query)) {\n if (v !== undefined && v !== null) url.searchParams.set(k, String(v));\n }\n }\n\n const headers: Record<string, string> = {\n Accept: 'application/json',\n 'User-Agent': this.userAgent,\n };\n if (auth && this.apiKey) headers['X-API-Key'] = this.apiKey;\n if (options.body !== undefined) headers['Content-Type'] = 'application/json';\n\n const res = await fetch(url, {\n method,\n headers,\n body: options.body !== undefined ? JSON.stringify(options.body) : undefined,\n });\n\n const text = await res.text();\n let parsed: unknown = null;\n if (text.length > 0) {\n try {\n parsed = JSON.parse(text);\n } catch {\n parsed = text;\n }\n }\n\n if (!res.ok) {\n throw new VsApiError(\n `Visual Sentinel API ${method} ${path} returned ${res.status}`,\n res.status,\n parsed,\n );\n }\n\n return parsed as T;\n }\n}\n"],"mappings":";;;AASA,IAAM,eAAe;AAQd,IAAM,aAAN,cAAyB,MAAM;AAAA,EACpC,YACE,SACgB,QACA,MAChB;AACA,UAAM,OAAO;AAHG;AACA;AAGhB,SAAK,OAAO;AAAA,EACd;AAAA,EALkB;AAAA,EACA;AAKpB;AAEO,IAAM,YAAN,MAAgB;AAAA,EACJ;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,OAAyB,CAAC,GAAG;AACvC,SAAK,SACH,KAAK,UACL,QAAQ,IAAI,cACZ,QAAQ,IAAI,0BACZ;AACF,SAAK,WAAW,KAAK,WAAW,QAAQ,IAAI,eAAe,cAAc;AAAA,MACvE;AAAA,MACA;AAAA,IACF;AACA,SAAK,YAAY,KAAK,aAAa;AAAA,EACrC;AAAA,EAEA,YAAqB;AACnB,WAAO,OAAO,KAAK,WAAW,YAAY,KAAK,OAAO,SAAS;AAAA,EACjE;AAAA,EAEA,MAAM,QACJ,QACA,MACA,UAA6G,CAAC,GAClG;AACZ,UAAM,OAAO,QAAQ,QAAQ;AAC7B,QAAI,QAAQ,CAAC,KAAK,UAAU,GAAG;AAC7B,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,UAAM,MAAM,IAAI,IAAI,KAAK,WAAW,GAAG,IAAI,OAAO,IAAI,IAAI,IAAI,KAAK,OAAO;AAC1E,QAAI,QAAQ,OAAO;AACjB,iBAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,QAAQ,KAAK,GAAG;AAClD,YAAI,MAAM,UAAa,MAAM,KAAM,KAAI,aAAa,IAAI,GAAG,OAAO,CAAC,CAAC;AAAA,MACtE;AAAA,IACF;AAEA,UAAM,UAAkC;AAAA,MACtC,QAAQ;AAAA,MACR,cAAc,KAAK;AAAA,IACrB;AACA,QAAI,QAAQ,KAAK,OAAQ,SAAQ,WAAW,IAAI,KAAK;AACrD,QAAI,QAAQ,SAAS,OAAW,SAAQ,cAAc,IAAI;AAE1D,UAAM,MAAM,MAAM,MAAM,KAAK;AAAA,MAC3B;AAAA,MACA;AAAA,MACA,MAAM,QAAQ,SAAS,SAAY,KAAK,UAAU,QAAQ,IAAI,IAAI;AAAA,IACpE,CAAC;AAED,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,QAAI,SAAkB;AACtB,QAAI,KAAK,SAAS,GAAG;AACnB,UAAI;AACF,iBAAS,KAAK,MAAM,IAAI;AAAA,MAC1B,QAAQ;AACN,iBAAS;AAAA,MACX;AAAA,IACF;AAEA,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,IAAI;AAAA,QACR,uBAAuB,MAAM,IAAI,IAAI,aAAa,IAAI,MAAM;AAAA,QAC5D,IAAI;AAAA,QACJ;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AACF;","names":[]}
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
- ListToolsRequestSchema
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 required. Set VS_API_KEY (https://visualsentinel.com/settings/api-keys).",
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
  );
@@ -189,7 +195,7 @@ var TOOLS = [
189
195
  idempotentHint: true,
190
196
  openWorldHint: true
191
197
  },
192
- description: "Run a one-shot performance check against a URL: TTFB, total load time, transfer size, status code, and redirect chain. No authentication required.",
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.",
193
199
  inputSchema: {
194
200
  type: "object",
195
201
  properties: {
@@ -558,7 +564,7 @@ var TOOLS = [
558
564
  idempotentHint: true,
559
565
  openWorldHint: false
560
566
  },
561
- description: "Acknowledge an alert by id. Acknowledgement is recorded with the calling API key's user.",
567
+ description: "Acknowledge an alert by id. The acknowledgement is recorded against the account that authorised this connection.",
562
568
  inputSchema: {
563
569
  type: "object",
564
570
  properties: {
@@ -621,9 +627,192 @@ function findTool(name) {
621
627
  return TOOLS.find((t) => t.name === name);
622
628
  }
623
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
+
624
814
  // src/index.ts
625
815
  var SERVER_NAME = "visualsentinel";
626
- var SERVER_VERSION = "0.2.1";
627
816
  function toMcpToolDescriptor(tool) {
628
817
  return {
629
818
  name: tool.name,
@@ -637,11 +826,47 @@ async function main() {
637
826
  const client = new ApiClient();
638
827
  const server = new Server(
639
828
  { name: SERVER_NAME, version: SERVER_VERSION },
640
- { capabilities: { tools: {} } }
829
+ { capabilities: { tools: {}, prompts: {}, resources: {} } }
641
830
  );
642
831
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
643
832
  tools: TOOLS.map(toMcpToolDescriptor)
644
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
+ });
645
870
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
646
871
  const tool = findTool(req.params.name);
647
872
  if (!tool) {