@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 +31 -0
- package/README.md +32 -7
- 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 +227 -11
- package/dist/index.js.map +1 -1
- package/dist/tools.d.ts +38 -0
- package/dist/tools.js +546 -0
- package/dist/tools.js.map +1 -0
- package/package.json +14 -10
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)
|
|
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
|
|
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
|
-
* **
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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: ["
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
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) =>
|
|
300
|
-
|
|
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
|
|
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
|
}
|