siteglass-mcp: agent-driven web QA MCP server

This commit is contained in:
2026-06-05 13:22:18 -04:00
commit f009ae212f
10 changed files with 1506 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
{
"name": "siteglass",
"description": "Agent-driven web QA: scan a site, auto-generate + run E2E test flows, read results + replay.",
"version": "0.1.0",
"author": { "name": "siteglass.io", "url": "https://siteglass.io" }
}
+1
View File
@@ -0,0 +1 @@
node_modules/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 siteglass.io
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+57
View File
@@ -0,0 +1,57 @@
# siteglass MCP server
[`siteglass-mcp`](https://www.npmjs.com/package/siteglass-mcp) exposes
[siteglass.io](https://siteglass.io)'s web-QA loop as MCP tools, so an AI
coding agent can test the app it just built — **register → verify → scan →
generate flows → run → read results** — autonomously, no human signup.
Auth is automatic (creates + caches an API key at `~/.siteglass/key`); set
`SITEGLASS_API_KEY` to use your own. The first scan per site is free; after
that, scans (2 credits) and flow runs (1 credit) draw from a prepaid balance
topped up in USD over Bitcoin Lightning.
## Install
```sh
npx siteglass-mcp
```
**Claude Code:**
```sh
claude mcp add siteglass -- npx siteglass-mcp
```
**Cursor / Claude Desktop / Windsurf** — add to the MCP config
(`~/.cursor/mcp.json`, `claude_desktop_config.json`, …):
```json
{
"mcpServers": {
"siteglass": { "command": "npx", "args": ["-y", "siteglass-mcp"] }
}
}
```
Then ask the agent: *"test my app at <url> with siteglass."*
## Tools
| tool | does |
|------|------|
| `siteglass_register_site` | register an app URL, get an ownership-proof token |
| `siteglass_verify_site` | confirm the token is live (DNS TXT or hosted) |
| `siteglass_scan` / `siteglass_get_scan` | crawl the site → report + findings (first scan free) |
| `siteglass_generate_flows` | derive executable E2E test flows from a scan |
| `siteglass_run_flow` / `siteglass_get_run` | run a flow → per-step pass/fail, screenshots, video, rrweb replay |
| `siteglass_set_credentials` | give a test login for authenticated flows |
| `siteglass_feedback` | send feedback to the siteglass team |
Long ops return an id and you poll the `get_*` tool, so every call stays
under MCP client request timeouts.
## Discovery
- Listed in the **official MCP Registry** as `io.siteglass/mcp`.
- Machine-discovery on the API host: `https://siteglass.io/openapi.json`,
`/.well-known/agent-skills/index.json`, `/llms.txt`, `/SKILLS.md`.
MIT licensed.
+5
View File
@@ -0,0 +1,5 @@
{
"mcpServers": {
"siteglass": { "command": "npx", "args": ["-y", "siteglass-mcp"] }
}
}
+1158
View File
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
{
"name": "siteglass-mcp",
"version": "0.1.0",
"description": "MCP server for siteglass.io — agent-driven web QA: register a site, scan it, auto-generate and run E2E test flows, read results + replay. Pay-per-action in credits over Bitcoin Lightning.",
"type": "module",
"bin": { "siteglass-mcp": "server.js" },
"main": "server.js",
"files": ["server.js", "README.md"],
"engines": { "node": ">=18" },
"keywords": [
"mcp",
"modelcontextprotocol",
"model-context-protocol",
"mcp-server",
"web-qa",
"testing",
"e2e",
"playwright",
"agent",
"ai-agent",
"site-testing",
"lightning",
"l402"
],
"homepage": "https://siteglass.io",
"repository": { "type": "git", "url": "git+https://git.sitelens.io/siteglass/siteglass-mcp.git" },
"bugs": { "url": "https://git.sitelens.io/siteglass/siteglass-mcp/issues" },
"author": "siteglass.io",
"license": "MIT",
"mcpName": "io.siteglass/mcp",
"dependencies": { "@modelcontextprotocol/sdk": "^1.0.0" }
}
+190
View File
@@ -0,0 +1,190 @@
#!/usr/bin/env node
// siteglass MCP server — exposes siteglass.io's web-QA loop as agent tools.
//
// An AI coding agent (Claude Code, Cursor, Claude Desktop, …) drives the
// whole loop over MCP: register the app it just built, prove ownership,
// scan it, derive + run E2E test flows, and read what broke.
//
// Tools return bounded (≤~50s) to stay under MCP client request timeouts:
// long ops (scan, run) return an id and you poll with siteglass_get_scan /
// siteglass_get_run.
//
// Config (env): SITEGLASS_BASE_URL (default https://siteglass.io),
// SITEGLASS_API_KEY (optional — auto-creates + caches one
// at ~/.siteglass/key if absent).
//
// Run: node server.js (stdio transport — how MCP clients launch it)
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
const BASE = (process.env.SITEGLASS_BASE_URL || "https://siteglass.io").replace(/\/$/, "");
const KEY_FILE = path.join(os.homedir(), ".siteglass", "key");
let KEY = process.env.SITEGLASS_API_KEY || null;
function loadKey() {
if (KEY) return KEY;
try { KEY = fs.readFileSync(KEY_FILE, "utf8").trim() || null; } catch {}
return KEY;
}
async function ensureKey() {
if (loadKey()) return KEY;
const r = await fetch(`${BASE}/api/signup`, { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" });
KEY = (await r.json()).api_key;
try { fs.mkdirSync(path.dirname(KEY_FILE), { recursive: true }); fs.writeFileSync(KEY_FILE, KEY, { mode: 0o600 }); } catch {}
return KEY;
}
async function api(method, p, body) {
await ensureKey();
const r = await fetch(BASE + p, {
method,
headers: { "Content-Type": "application/json", Authorization: "Bearer " + KEY },
body: body ? JSON.stringify(body) : undefined,
});
const txt = await r.text();
let j; try { j = JSON.parse(txt); } catch { j = { ok: false, error: txt.slice(0, 200) }; }
return j;
}
// Poll a status endpoint, but give up after maxMs so the tool call returns
// inside the client's request timeout. Returns the last response seen.
async function poll(p, { maxMs = 50000, ms = 2500 } = {}) {
const deadline = Date.now() + maxMs;
let last = await api("GET", p);
while (Date.now() < deadline && last.ok && last.status !== "DONE" && last.status !== "ERROR") {
await new Promise((res) => setTimeout(res, ms));
last = await api("GET", p);
}
return last;
}
const text = (s) => ({ content: [{ type: "text", text: typeof s === "string" ? s : JSON.stringify(s, null, 2) }] });
function scanSummary(j) {
const f = (j.findings || []).map((x) => ` [${x.kind}/${x.severity}] ${x.title}`).join("\n");
return `crawl_id: ${j.crawl_id}\n${j.page_count} pages (${j.discovered} discovered)\n\n` +
`FINDINGS (${(j.findings || []).length}):\n${f}\n\nREPORT:\n${j.report_md || ""}\n\n` +
`Next: siteglass_generate_flows with crawl_id=${j.crawl_id}.`;
}
function runSummary(j) {
const steps = (j.steps || []).map((s) =>
` [${s.status}] ${s.action}${s.description}` + (s.error ? ` (${s.error})` : "") +
(s.status === "repaired" ? ` [repaired: ${JSON.stringify(s.target)} -> ${s.repaired_to}]` : "")).join("\n");
return `${j.passed}${j.failed}${j.skipped} skipped ${j.repaired} repaired\n${steps}\n\n` +
`replay: ${BASE}${j.rrweb_url}\nvideo: ${BASE}${j.video_url}`;
}
const server = new McpServer({ name: "siteglass", version: "0.1.0" });
server.tool(
"siteglass_register_site",
"Register a web app (its deployed URL) with siteglass and get an ownership-proof token to place on it. Step 1.",
{ url: z.string().describe("The app's URL, e.g. https://myapp.lovable.app") },
async ({ url }) => {
const j = await api("POST", "/api/sites", { domain: url });
if (!j.ok) return text(`Error: ${j.error}`);
const i = j.instructions || {};
return text(`Registered. site_id: ${j.site_id}\n\nProve you control ${j.domain} with ANY ONE of:\n` +
`• Meta tag (easiest for AI-built apps): ${i.meta_tag}\n• Hosted file: ${i.well_known}\n• DNS: ${i.dns_txt}\n\n` +
`Add one (the meta tag goes in the homepage <head>), deploy, then call siteglass_verify_site with site_id=${j.site_id}.`);
}
);
server.tool(
"siteglass_verify_site",
"Check that the ownership-proof token is live on the site. Step 2 (after placing the token).",
{ site_id: z.string() },
async ({ site_id }) => {
const j = await api("POST", `/api/sites/${site_id}/verify`);
if (!j.ok) return text(`Error: ${j.error}`);
return text(j.verified ? `Verified via ${j.method}. Now call siteglass_scan with site_id=${site_id}.`
: `Not verified yet — the token isn't reachable. Place it, redeploy, and retry.`);
}
);
server.tool(
"siteglass_scan",
"Crawl a verified site (first scan per site is free). Returns a crawl_id; if still running, poll with siteglass_get_scan. Step 3.",
{ site_id: z.string() },
async ({ site_id }) => {
const start = await api("POST", `/api/sites/${site_id}/crawl`);
if (!start.ok) return text(`Cannot scan: ${start.error}`);
const j = await poll(`/api/crawls/${start.crawl_id}`);
if (j.status === "DONE") return text(scanSummary(j));
return text(`Scan started. crawl_id: ${start.crawl_id} (status ${j.status || "RUNNING"}). ` +
`Call siteglass_get_scan with crawl_id=${start.crawl_id} in ~20s.`);
}
);
server.tool(
"siteglass_get_scan",
"Read a scan's report + findings by crawl_id (use after siteglass_scan if it was still running).",
{ crawl_id: z.string() },
async ({ crawl_id }) => {
const j = await api("GET", `/api/crawls/${crawl_id}`);
if (!j.ok) return text(`Error: ${j.error}`);
return text(j.status === "DONE" ? scanSummary(j) : `Scan status: ${j.status}. Check again shortly.`);
}
);
server.tool(
"siteglass_generate_flows",
"Derive executable E2E test flows from a scan (LLM). Step 4. Returns flow ids.",
{ crawl_id: z.string() },
async ({ crawl_id }) => {
const j = await api("POST", `/api/crawls/${crawl_id}/flows`);
if (!j.ok) return text(`Error: ${j.error}`);
return text(`Generated ${j.count} flows:\n` +
(j.flows || []).map((x) => ` ${x.id} [${x.destructive ? "destructive" : "safe"}] ${x.name}${x.description}`).join("\n") +
`\n\nRun one with siteglass_run_flow. Destructive flows: full=false dry-runs the final submit; full=true submits for real (a passing Run Full of a sign-up flow auto-saves the new account as this site's test credentials).`);
}
);
server.tool(
"siteglass_run_flow",
"Run one test flow. Returns a run_id; if still running, poll with siteglass_get_run. Step 5.",
{ flow_id: z.string(), full: z.boolean().optional().describe("true = submit destructive actions for real (default false)") },
async ({ flow_id, full }) => {
const start = await api("POST", `/api/flows/${flow_id}/run`, { full: !!full });
if (!start.ok) return text(`Cannot run: ${start.error}`);
const j = await poll(`/api/flow-runs/${start.run_id}`);
if (j.status === "DONE") return text(runSummary(j));
return text(`Run started. run_id: ${start.run_id} (status ${j.status || "RUNNING"}). ` +
`Call siteglass_get_run with run_id=${start.run_id} in ~20s.`);
}
);
server.tool(
"siteglass_get_run",
"Read a flow run's per-step results + capture URLs by run_id (use after siteglass_run_flow if still running).",
{ run_id: z.string() },
async ({ run_id }) => {
const j = await api("GET", `/api/flow-runs/${run_id}`);
if (!j.ok) return text(`Error: ${j.error}`);
return text(j.status === "DONE" ? runSummary(j) : `Run status: ${j.status}. Check again shortly.`);
}
);
server.tool(
"siteglass_set_credentials",
"Provide a test account (username/password) so authenticated flows can log in to the site.",
{ site_id: z.string(), username: z.string(), password: z.string() },
async ({ site_id, username, password }) => {
const j = await api("POST", `/api/sites/${site_id}/credentials`, { vars: { username, password } });
return text(j.ok ? "Test credentials saved." : `Error: ${j.error}`);
}
);
server.tool(
"siteglass_feedback",
"Send feedback to the siteglass team (bugs, missing features, anything). Works for agents and people alike.",
{ message: z.string(), context: z.string().optional() },
async ({ message, context }) => {
const j = await api("POST", "/api/feedback", { message, context });
return text(j.ok ? "Thanks — feedback received." : `Error: ${j.error}`);
}
);
await server.connect(new StdioServerTransport());
+18
View File
@@ -0,0 +1,18 @@
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "io.siteglass/mcp",
"description": "Agent-driven web QA: scan a site, auto-generate + run E2E test flows, read results.",
"version": "0.1.0",
"packages": [
{
"registryType": "npm",
"identifier": "siteglass-mcp",
"version": "0.1.0",
"transport": { "type": "stdio" },
"environmentVariables": [
{ "name": "SITEGLASS_API_KEY", "description": "siteglass API key (optional; the server auto-creates an account if absent).", "isRequired": false, "isSecret": true },
{ "name": "SITEGLASS_BASE_URL", "description": "API base URL (default https://siteglass.io).", "isRequired": false }
]
}
]
}
+18
View File
@@ -0,0 +1,18 @@
# Smithery deploy config (only needed if you list on Smithery, which builds
# from a public GitHub repo). The npm + official-registry path does NOT need this.
startCommand:
type: stdio
configSchema:
type: object
properties:
siteglassApiKey:
type: string
title: siteglass API key
description: Optional. The server auto-creates an account and caches a key if omitted.
required: []
commandFunction: |-
(config) => ({
command: 'node',
args: ['server.js'],
env: config.siteglassApiKey ? { SITEGLASS_API_KEY: config.siteglassApiKey } : {}
})