siteglass-mcp: agent-driven web QA MCP server
This commit is contained in:
@@ -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" }
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
node_modules/
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"siteglass": { "command": "npx", "args": ["-y", "siteglass-mcp"] }
|
||||
}
|
||||
}
|
||||
Generated
+1158
File diff suppressed because it is too large
Load Diff
@@ -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" }
|
||||
}
|
||||
@@ -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
@@ -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 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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 } : {}
|
||||
})
|
||||
Reference in New Issue
Block a user