@visualsentinel/mcp-server 0.1.0 → 0.2.1

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,37 @@
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.1] - 2026-10-16
6
+
7
+ ### Fixed
8
+
9
+ * `vs_ssl_check` sent `?host=` but the API reads `?domain=`, so every call returned a 400. The tool now takes `domain` (`host` still works as an alias). The unsupported `port` input is removed.
10
+ * `vs_dns_check` sent `recordType`, which the API ignored. It now sends `type`, so the record-type filter works.
11
+ * The server now reports its real version in the MCP handshake instead of a fixed `0.1.0`.
12
+
13
+ ### Added
14
+
15
+ * Every tool has a `title` and the MCP annotations `readOnlyHint`, `destructiveHint`, `idempotentHint` and `openWorldHint`, as AI directories require.
16
+ * `mcpName` (`io.github.VisualSentinel/mcp-server`) and `server.json` for the official MCP Registry.
17
+ * `.cursor-plugin/plugin.json`, `.claude-plugin/plugin.json`, `.mcp.json` and `mcp.json` for the Cursor Marketplace and Grok Bot, `llms-install.md` for Cline, a `Dockerfile` for the Docker MCP Catalog and Glama, and `gemini-extension.json`.
18
+
19
+ ## [0.2.0] - 2026-06-04
20
+
21
+ ### Added
22
+
23
+ * New tool `vs_monitors_metrics_bulk`: fetches uptime or response-time metrics for up to 100 monitors in a single API call. Accepts `monitorIds` (array), `metric` (`uptime` | `response-time`), and either a preset `window` or custom `from`+`to` ISO 8601 range. POSTs to `POST /api/monitors/metrics`.
24
+ * `vs_monitors_uptime`: added `from` and `to` input properties for custom date ranges. When both are supplied the handler sends them as query params instead of `period`, allowing arbitrary historical windows.
25
+ * `all` (boolean) input on `vs_monitors_uptime` and `vs_monitors_metrics_bulk`: requests the full available history from the earliest stored data. Precedence mirrors the API: `all` > `from`/`to` > `window`/`period`. This makes the documented full-history capability actually reachable through the tools.
26
+
27
+ ### Changed
28
+
29
+ * `vs_monitors_uptime`: input schema now includes `from`, `to`, and expands `window` description to list `1h | 6h | 24h | 7d | 30d | 90d | 365d`. Handler now correctly maps `window` → `period` query param (previously `window` was sent as-is and silently ignored by the API). Default period remains `30d`.
30
+ * `tsup.config.ts`: added `src/tools.ts` and `src/api-client.ts` as explicit entry points so they emit separate dist files importable by tests without bundling index.ts.
31
+
32
+ ### Fixed
33
+
34
+ * `vs_monitors_uptime` handler was sending `{ window: ... }` in the query string instead of `{ period: ... }`. The Visual Sentinel API only accepts `period`; the old behaviour silently fell back to the server default.
35
+
5
36
  ## [0.1.0] - 2026-04-29
6
37
 
7
38
  Initial public release.
package/README.md CHANGED
@@ -1,17 +1,17 @@
1
1
  # Visual Sentinel MCP Server
2
2
 
3
- Model Context Protocol server for [Visual Sentinel](https://visualsentinel.com), the six-layer website monitoring platform: uptime, performance, SSL, DNS, visual regression, and content change detection.
3
+ 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
4
 
5
- Connect Claude Desktop, Claude Code, Cline, Continue, or any MCP-compatible client to your Visual Sentinel account so an AI assistant can list monitors, create them, fetch incident history, run free DNS / SSL / speed checks, and acknowledge alerts on your behalf.
5
+ 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
6
 
7
7
  * **npm:** `@visualsentinel/mcp-server`
8
8
  * **OpenAPI spec:** https://github.com/VisualSentinel/openapi
9
- * **Get an API key:** https://visualsentinel.com/settings/api-keys
9
+ * **API key (Business and Agency plans):** https://visualsentinel.com/settings/api-keys
10
10
  * **Hosted API reference:** https://visualsentinel.com/docs
11
11
 
12
12
  ## What you get
13
13
 
14
- 16 tools spanning the public Visual Sentinel API:
14
+ 17 tools spanning the public Visual Sentinel API:
15
15
 
16
16
  **Public tools (no API key required)**
17
17
 
@@ -31,7 +31,8 @@ Connect Claude Desktop, Claude Code, Cline, Continue, or any MCP-compatible clie
31
31
  | `vs_monitors_get` | `GET /api/monitors/{id}` | Fetch a single monitor with current status |
32
32
  | `vs_monitors_create` | `POST /api/monitors` | Create a new monitor |
33
33
  | `vs_monitors_check_now` | `POST /api/monitors/{id}/check` | Trigger an immediate check |
34
- | `vs_monitors_uptime` | `GET /api/monitors/{id}/uptime` | Uptime percentage over 24h / 7d / 30d / 90d / 365d |
34
+ | `vs_monitors_uptime` | `GET /api/monitors/{id}/uptime` | Uptime percentage for a preset window, a custom date range, or the full history |
35
+ | `vs_monitors_metrics_bulk` | `POST /api/monitors/metrics` | Uptime or response-time metrics for up to 100 monitors in one call |
35
36
  | `vs_incidents_list` | `GET /api/incidents` | List incidents, filterable by status or monitor |
36
37
  | `vs_incidents_get` | `GET /api/incidents/{id}` | Fetch an incident with full check history |
37
38
  | `vs_alerts_list` | `GET /api/alerts` | List alerts by status |
@@ -59,7 +60,7 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
59
60
  }
60
61
  ```
61
62
 
62
- Restart Claude Desktop. The 16 tools appear under the tools menu.
63
+ Restart Claude Desktop. The 17 tools appear under the tools menu.
63
64
 
64
65
  ### Claude Code (CLI)
65
66
 
@@ -70,6 +71,30 @@ claude mcp add visualsentinel \
70
71
 
71
72
  Then export `VS_API_KEY` in the shell where Claude Code runs.
72
73
 
74
+ ### Cursor and Grok Bot
75
+
76
+ This repository is also an installable plugin: `.cursor-plugin/plugin.json` and `mcp.json` for the Cursor Marketplace, and `.claude-plugin/plugin.json` and `.mcp.json` for Grok and Claude Code (`grok plugin install VisualSentinel/mcp-server`). Or add the server by hand to `~/.cursor/mcp.json`:
77
+
78
+ ```json
79
+ {
80
+ "mcpServers": {
81
+ "visualsentinel": {
82
+ "command": "npx",
83
+ "args": ["-y", "@visualsentinel/mcp-server"]
84
+ }
85
+ }
86
+ }
87
+ ```
88
+
89
+ ### Docker
90
+
91
+ ```bash
92
+ docker build -t visualsentinel-mcp-server .
93
+ docker run -i --rm -e VS_API_KEY visualsentinel-mcp-server
94
+ ```
95
+
96
+ The server also ships as a Gemini CLI extension (`gemini-extension.json`).
97
+
73
98
  ### Cline (VS Code extension)
74
99
 
75
100
  In Cline's MCP settings, add:
@@ -115,7 +140,7 @@ Two environment variables, both optional except in the noted cases:
115
140
 
116
141
  | Variable | Default | Purpose |
117
142
  |---|---|---|
118
- | `VS_API_KEY` | (none) | Required for the 11 authenticated tools. Public tools work without it. Generate at https://visualsentinel.com/settings/api-keys. |
143
+ | `VS_API_KEY` | (none) | Required for the 12 authenticated tools (API keys are available on the Business and Agency plans). Public tools work without it. Generate at https://visualsentinel.com/settings/api-keys. |
119
144
  | `VS_API_BASE` | `https://visualsentinel.com` | Override only for testing against a local development instance. |
120
145
 
121
146
  If a tool that needs auth is called without `VS_API_KEY`, the server returns a clear error rather than silently failing.
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Tiny HTTP client for the Visual Sentinel public API.
3
+ *
4
+ * Reads the API key from `VS_API_KEY` (or `VISUALSENTINEL_API_KEY`) once at
5
+ * construction time. All requests are routed at `https://visualsentinel.com`
6
+ * unless `VS_API_BASE` overrides it (useful for testing against a local
7
+ * dev instance).
8
+ */
9
+ interface ApiClientOptions {
10
+ apiKey?: string | undefined;
11
+ baseUrl?: string | undefined;
12
+ userAgent?: string;
13
+ }
14
+ declare class VsApiError extends Error {
15
+ readonly status: number;
16
+ readonly body: unknown;
17
+ constructor(message: string, status: number, body: unknown);
18
+ }
19
+ declare class ApiClient {
20
+ private readonly apiKey;
21
+ private readonly baseUrl;
22
+ private readonly userAgent;
23
+ constructor(opts?: ApiClientOptions);
24
+ hasApiKey(): boolean;
25
+ request<T = unknown>(method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE', path: string, options?: {
26
+ query?: Record<string, string | number | boolean | undefined>;
27
+ body?: unknown;
28
+ auth?: boolean;
29
+ }): Promise<T>;
30
+ }
31
+
32
+ export { ApiClient, type ApiClientOptions, VsApiError };
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/api-client.ts
4
+ var DEFAULT_BASE = "https://visualsentinel.com";
5
+ var VsApiError = class extends Error {
6
+ constructor(message, status, body) {
7
+ super(message);
8
+ this.status = status;
9
+ this.body = body;
10
+ this.name = "VsApiError";
11
+ }
12
+ status;
13
+ body;
14
+ };
15
+ var ApiClient = class {
16
+ apiKey;
17
+ baseUrl;
18
+ userAgent;
19
+ constructor(opts = {}) {
20
+ this.apiKey = opts.apiKey ?? process.env.VS_API_KEY ?? process.env.VISUALSENTINEL_API_KEY ?? void 0;
21
+ this.baseUrl = (opts.baseUrl ?? process.env.VS_API_BASE ?? DEFAULT_BASE).replace(
22
+ /\/+$/,
23
+ ""
24
+ );
25
+ this.userAgent = opts.userAgent ?? "@visualsentinel/mcp-server";
26
+ }
27
+ hasApiKey() {
28
+ return typeof this.apiKey === "string" && this.apiKey.length > 0;
29
+ }
30
+ async request(method, path, options = {}) {
31
+ const auth = options.auth ?? true;
32
+ if (auth && !this.hasApiKey()) {
33
+ throw new VsApiError(
34
+ "Visual Sentinel API key required. Set VS_API_KEY (https://visualsentinel.com/settings/api-keys).",
35
+ 401,
36
+ null
37
+ );
38
+ }
39
+ const url = new URL(path.startsWith("/") ? path : `/${path}`, this.baseUrl);
40
+ if (options.query) {
41
+ for (const [k, v] of Object.entries(options.query)) {
42
+ if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
43
+ }
44
+ }
45
+ const headers = {
46
+ Accept: "application/json",
47
+ "User-Agent": this.userAgent
48
+ };
49
+ if (auth && this.apiKey) headers["X-API-Key"] = this.apiKey;
50
+ if (options.body !== void 0) headers["Content-Type"] = "application/json";
51
+ const res = await fetch(url, {
52
+ method,
53
+ headers,
54
+ body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
55
+ });
56
+ const text = await res.text();
57
+ let parsed = null;
58
+ if (text.length > 0) {
59
+ try {
60
+ parsed = JSON.parse(text);
61
+ } catch {
62
+ parsed = text;
63
+ }
64
+ }
65
+ if (!res.ok) {
66
+ throw new VsApiError(
67
+ `Visual Sentinel API ${method} ${path} returned ${res.status}`,
68
+ res.status,
69
+ parsed
70
+ );
71
+ }
72
+ return parsed;
73
+ }
74
+ };
75
+ export {
76
+ ApiClient,
77
+ VsApiError
78
+ };
79
+ //# sourceMappingURL=api-client.js.map
@@ -0,0 +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":[]}
package/dist/index.js CHANGED
@@ -103,6 +103,14 @@ var TOOLS = [
103
103
  // -------------------------------------------------------------------------
104
104
  {
105
105
  name: "vs_health",
106
+ title: "Check Visual Sentinel service health",
107
+ annotations: {
108
+ title: "Check Visual Sentinel service health",
109
+ readOnlyHint: true,
110
+ destructiveHint: false,
111
+ idempotentHint: true,
112
+ openWorldHint: true
113
+ },
106
114
  description: "Check whether Visual Sentinel itself is up. Returns the service health status. No authentication required.",
107
115
  inputSchema: {
108
116
  type: "object",
@@ -114,6 +122,14 @@ var TOOLS = [
114
122
  },
115
123
  {
116
124
  name: "vs_dns_check",
125
+ title: "Look up DNS records",
126
+ annotations: {
127
+ title: "Look up DNS records",
128
+ readOnlyHint: true,
129
+ destructiveHint: false,
130
+ idempotentHint: true,
131
+ openWorldHint: true
132
+ },
117
133
  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
134
  inputSchema: {
119
135
  type: "object",
@@ -132,30 +148,47 @@ var TOOLS = [
132
148
  auth: false,
133
149
  query: {
134
150
  domain: requireString(args, "domain"),
135
- recordType: pickString(args, "recordType")
151
+ // The API reads ?type= (not recordType).
152
+ type: pickString(args, "recordType")
136
153
  }
137
154
  })
138
155
  },
139
156
  {
140
157
  name: "vs_ssl_check",
158
+ title: "Inspect an SSL certificate",
159
+ annotations: {
160
+ title: "Inspect an SSL certificate",
161
+ readOnlyHint: true,
162
+ destructiveHint: false,
163
+ idempotentHint: true,
164
+ openWorldHint: true
165
+ },
141
166
  description: "Inspect a TLS/SSL certificate for a hostname: issuer, subject, validity dates, SAN list, key algorithm, and certificate chain. No authentication required.",
142
167
  inputSchema: {
143
168
  type: "object",
144
169
  properties: {
145
- host: { ...STR, description: "Hostname to inspect, e.g. example.com (without protocol)." },
146
- port: { ...INT, description: "TCP port. Default 443." }
170
+ domain: { ...STR, description: "Hostname to inspect, e.g. example.com (without protocol)." }
147
171
  },
148
- required: ["host"],
172
+ required: ["domain"],
149
173
  additionalProperties: false
150
174
  },
151
175
  requiresAuth: false,
152
176
  handler: async (args, client) => client.request("GET", "/api/tools/ssl-check", {
153
177
  auth: false,
154
- query: { host: requireString(args, "host"), port: pickNumber(args, "port") }
178
+ // The API reads ?domain=. `host` stays as an input alias for 0.1.x callers.
179
+ query: { domain: pickString(args, "domain") ?? requireString(args, "host") }
155
180
  })
156
181
  },
157
182
  {
158
183
  name: "vs_speed_test",
184
+ title: "Run a one-shot speed test",
185
+ annotations: {
186
+ title: "Run a one-shot speed test",
187
+ readOnlyHint: true,
188
+ destructiveHint: false,
189
+ idempotentHint: true,
190
+ openWorldHint: true
191
+ },
159
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.",
160
193
  inputSchema: {
161
194
  type: "object",
@@ -173,6 +206,14 @@ var TOOLS = [
173
206
  },
174
207
  {
175
208
  name: "vs_website_check",
209
+ title: "Check a website",
210
+ annotations: {
211
+ title: "Check a website",
212
+ readOnlyHint: true,
213
+ destructiveHint: false,
214
+ idempotentHint: true,
215
+ openWorldHint: true
216
+ },
176
217
  description: "Quick health check for a URL: HTTP status, response time, server header, content snippet, and basic SSL state. No authentication required.",
177
218
  inputSchema: {
178
219
  type: "object",
@@ -193,6 +234,14 @@ var TOOLS = [
193
234
  // -------------------------------------------------------------------------
194
235
  {
195
236
  name: "vs_monitors_list",
237
+ title: "List monitors",
238
+ annotations: {
239
+ title: "List monitors",
240
+ readOnlyHint: true,
241
+ destructiveHint: false,
242
+ idempotentHint: true,
243
+ openWorldHint: false
244
+ },
196
245
  description: "List monitors in the authenticated organization. Optional filters narrow by status, type, or paginate.",
197
246
  inputSchema: {
198
247
  type: "object",
@@ -219,6 +268,14 @@ var TOOLS = [
219
268
  },
220
269
  {
221
270
  name: "vs_monitors_get",
271
+ title: "Get a monitor",
272
+ annotations: {
273
+ title: "Get a monitor",
274
+ readOnlyHint: true,
275
+ destructiveHint: false,
276
+ idempotentHint: true,
277
+ openWorldHint: false
278
+ },
222
279
  description: "Fetch a single monitor by id, including its current status, last check, and configuration.",
223
280
  inputSchema: {
224
281
  type: "object",
@@ -233,6 +290,14 @@ var TOOLS = [
233
290
  },
234
291
  {
235
292
  name: "vs_monitors_create",
293
+ title: "Create a monitor",
294
+ annotations: {
295
+ title: "Create a monitor",
296
+ readOnlyHint: false,
297
+ destructiveHint: false,
298
+ idempotentHint: false,
299
+ openWorldHint: false
300
+ },
236
301
  description: "Create a new monitor. Provide at least name, url, and type. Returns the created monitor including its assigned id.",
237
302
  inputSchema: {
238
303
  type: "object",
@@ -268,6 +333,14 @@ var TOOLS = [
268
333
  },
269
334
  {
270
335
  name: "vs_monitors_check_now",
336
+ title: "Run a monitor check now",
337
+ annotations: {
338
+ title: "Run a monitor check now",
339
+ readOnlyHint: false,
340
+ destructiveHint: false,
341
+ idempotentHint: false,
342
+ openWorldHint: false
343
+ },
271
344
  description: "Trigger an immediate check for a monitor (in addition to its scheduled cadence). Returns the freshly-collected check result.",
272
345
  inputSchema: {
273
346
  type: "object",
@@ -282,26 +355,127 @@ var TOOLS = [
282
355
  },
283
356
  {
284
357
  name: "vs_monitors_uptime",
285
- description: "Fetch the uptime percentage and outage breakdown for a monitor over a window (default last 30 days).",
358
+ title: "Get monitor uptime",
359
+ annotations: {
360
+ title: "Get monitor uptime",
361
+ readOnlyHint: true,
362
+ destructiveHint: false,
363
+ idempotentHint: true,
364
+ openWorldHint: false
365
+ },
366
+ 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
367
  inputSchema: {
287
368
  type: "object",
288
369
  properties: {
289
370
  id: { ...STR, description: "Monitor id." },
290
371
  window: {
291
372
  ...STR,
292
- description: "Window: 24h, 7d, 30d, 90d, 365d. Default 30d."
373
+ description: "Preset window: 1h, 6h, 24h, 7d, 30d, 90d, 365d. Default 30d. Ignored when from+to or all are supplied."
374
+ },
375
+ from: {
376
+ ...STR,
377
+ description: "ISO 8601 start of a custom range (e.g. 2026-01-01T00:00:00Z). Must be supplied with `to`."
378
+ },
379
+ to: {
380
+ ...STR,
381
+ description: "ISO 8601 end of a custom range. Must be supplied with `from`."
382
+ },
383
+ all: {
384
+ type: "boolean",
385
+ description: "Return the full available history from the earliest stored data. Takes precedence over from/to and window."
293
386
  }
294
387
  },
295
388
  required: ["id"],
296
389
  additionalProperties: false
297
390
  },
298
391
  requiresAuth: true,
299
- handler: async (args, client) => client.request("GET", `/api/monitors/${encodeURIComponent(requireString(args, "id"))}/uptime`, {
300
- query: { window: pickString(args, "window") }
301
- })
392
+ handler: async (args, client) => {
393
+ const from = pickString(args, "from");
394
+ const to = pickString(args, "to");
395
+ const query = args["all"] === true ? { all: true } : from && to ? { from, to } : { period: pickString(args, "window") ?? "30d" };
396
+ return client.request(
397
+ "GET",
398
+ `/api/monitors/${encodeURIComponent(requireString(args, "id"))}/uptime`,
399
+ { query }
400
+ );
401
+ }
402
+ },
403
+ {
404
+ name: "vs_monitors_metrics_bulk",
405
+ title: "Get bulk monitor metrics",
406
+ annotations: {
407
+ title: "Get bulk monitor metrics",
408
+ readOnlyHint: true,
409
+ destructiveHint: false,
410
+ idempotentHint: true,
411
+ openWorldHint: false
412
+ },
413
+ 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.",
414
+ inputSchema: {
415
+ type: "object",
416
+ properties: {
417
+ monitorIds: {
418
+ type: "array",
419
+ items: { type: "string" },
420
+ description: "List of monitor IDs to fetch metrics for (max 100)."
421
+ },
422
+ metric: {
423
+ ...STR,
424
+ description: 'Metric to return: "uptime" or "response-time".'
425
+ },
426
+ window: {
427
+ ...STR,
428
+ description: "Preset window: 1h, 6h, 24h, 7d, 30d, 90d, 365d. Default 30d. Ignored when from+to or all are supplied."
429
+ },
430
+ from: {
431
+ ...STR,
432
+ description: "ISO 8601 start of a custom range (e.g. 2026-01-01T00:00:00Z). Must be supplied with `to`."
433
+ },
434
+ to: {
435
+ ...STR,
436
+ description: "ISO 8601 end of a custom range. Must be supplied with `from`."
437
+ },
438
+ all: {
439
+ type: "boolean",
440
+ description: "Return the full available history from the earliest stored data. Takes precedence over from/to and window."
441
+ }
442
+ },
443
+ required: ["monitorIds", "metric"],
444
+ additionalProperties: false
445
+ },
446
+ requiresAuth: true,
447
+ handler: async (args, client) => {
448
+ const monitorIds = args["monitorIds"];
449
+ if (!Array.isArray(monitorIds) || monitorIds.length === 0) {
450
+ throw new Error('Argument "monitorIds" must be a non-empty array.');
451
+ }
452
+ const from = pickString(args, "from");
453
+ const to = pickString(args, "to");
454
+ const body = {
455
+ monitorIds,
456
+ metric: requireString(args, "metric")
457
+ };
458
+ if (args["all"] === true) {
459
+ body["all"] = true;
460
+ } else if (from && to) {
461
+ body["from"] = from;
462
+ body["to"] = to;
463
+ } else {
464
+ body["period"] = pickString(args, "window") ?? "30d";
465
+ }
466
+ return client.request("POST", "/api/monitors/metrics", { body });
467
+ }
302
468
  },
303
469
  {
304
470
  name: "vs_incidents_list",
471
+ title: "List incidents",
472
+ annotations: {
473
+ title: "List incidents",
474
+ readOnlyHint: true,
475
+ destructiveHint: false,
476
+ idempotentHint: true,
477
+ openWorldHint: false
478
+ },
305
479
  description: "List incidents for the authenticated organization. Filter by status (OPEN/RESOLVED), monitor id, or paginate.",
306
480
  inputSchema: {
307
481
  type: "object",
@@ -325,6 +499,14 @@ var TOOLS = [
325
499
  },
326
500
  {
327
501
  name: "vs_incidents_get",
502
+ title: "Get an incident",
503
+ annotations: {
504
+ title: "Get an incident",
505
+ readOnlyHint: true,
506
+ destructiveHint: false,
507
+ idempotentHint: true,
508
+ openWorldHint: false
509
+ },
328
510
  description: "Fetch a single incident with its full check history and root-cause hints.",
329
511
  inputSchema: {
330
512
  type: "object",
@@ -339,6 +521,14 @@ var TOOLS = [
339
521
  },
340
522
  {
341
523
  name: "vs_alerts_list",
524
+ title: "List alerts",
525
+ annotations: {
526
+ title: "List alerts",
527
+ readOnlyHint: true,
528
+ destructiveHint: false,
529
+ idempotentHint: true,
530
+ openWorldHint: false
531
+ },
342
532
  description: "List alerts for the authenticated organization. Filter by status (UNACKNOWLEDGED/ACKNOWLEDGED/RESOLVED) or paginate.",
343
533
  inputSchema: {
344
534
  type: "object",
@@ -360,6 +550,14 @@ var TOOLS = [
360
550
  },
361
551
  {
362
552
  name: "vs_alerts_acknowledge",
553
+ title: "Acknowledge an alert",
554
+ annotations: {
555
+ title: "Acknowledge an alert",
556
+ readOnlyHint: false,
557
+ destructiveHint: false,
558
+ idempotentHint: true,
559
+ openWorldHint: false
560
+ },
363
561
  description: "Acknowledge an alert by id. Acknowledgement is recorded with the calling API key's user.",
364
562
  inputSchema: {
365
563
  type: "object",
@@ -377,6 +575,14 @@ var TOOLS = [
377
575
  },
378
576
  {
379
577
  name: "vs_status_pages_list",
578
+ title: "List status pages",
579
+ annotations: {
580
+ title: "List status pages",
581
+ readOnlyHint: true,
582
+ destructiveHint: false,
583
+ idempotentHint: true,
584
+ openWorldHint: false
585
+ },
380
586
  description: "List public-facing status pages owned by the authenticated organization.",
381
587
  inputSchema: {
382
588
  type: "object",
@@ -388,6 +594,14 @@ var TOOLS = [
388
594
  },
389
595
  {
390
596
  name: "vs_servers_list",
597
+ title: "List servers",
598
+ annotations: {
599
+ title: "List servers",
600
+ readOnlyHint: true,
601
+ destructiveHint: false,
602
+ idempotentHint: true,
603
+ openWorldHint: false
604
+ },
391
605
  description: "List servers (origins) registered in the authenticated organization.",
392
606
  inputSchema: {
393
607
  type: "object",
@@ -409,11 +623,13 @@ function findTool(name) {
409
623
 
410
624
  // src/index.ts
411
625
  var SERVER_NAME = "visualsentinel";
412
- var SERVER_VERSION = "0.1.0";
626
+ var SERVER_VERSION = "0.2.1";
413
627
  function toMcpToolDescriptor(tool) {
414
628
  return {
415
629
  name: tool.name,
630
+ title: tool.title,
416
631
  description: tool.description,
632
+ annotations: tool.annotations,
417
633
  inputSchema: tool.inputSchema
418
634
  };
419
635
  }