webaudit-mcp
Audits public web pages for tracking tags, duplicate pixels, page weight, header hygiene, and lab performance from inside Claude Code or Claude Desktop.
README
webaudit-mcp
MCP server that audits any public web page for tracking tags, duplicate pixels, page weight, header hygiene, and lab performance, from inside Claude Code or Claude Desktop.
![]()
| Screenshot | What it shows |
|---|---|
| smoke-tracking-summary.png | Real npm run smoke -- https://web.dev session: the tools/list handshake and the full tracking_summary report (GTM container found, no consent management, no duplicates). |
| ssrf-refusal.png | The same smoke script pointed at http://localhost:8080: every tool returns a structured SSRF refusal instead of fetching, including the cloud metadata IP 169.254.169.254. |
Both screenshots are captured from real sessions on 2026-07-19 against the current build. The second one is a selling point, not an error state: point any tool at a private, broken, or non-HTML URL and you get a structured, actionable error, never a crash or a hang.
Error: Refusing to fetch "localhost": local targets are blocked (SSRF protection).
Quickstart
Requires Node 20 or newer. From a clean clone:
git clone https://github.com/lpecom/webaudit-mcp.git webaudit-mcp
cd webaudit-mcp
npm install
npm run build # compiles TypeScript into dist/
npm test # 60 tests across 6 files, all offline, ~300 ms
Starting the server directly prints one line to stderr and then waits for an MCP client on stdio (stop it with Ctrl+C):
node dist/index.js
# webaudit-mcp running on stdio (7 tools registered)
Claude Code:
claude mcp add webaudit -- node /absolute/path/to/webaudit-mcp/dist/index.js
Claude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"webaudit": {
"command": "node",
"args": ["/absolute/path/to/webaudit-mcp/dist/index.js"]
}
}
}
Then ask: "Audit https://example.com" or "What tracking is installed on web.dev?"
Optional, only needed for measure_performance (lab Core Web Vitals):
npx playwright install chromium
To see every tool fire against a live site without any MCP client:
npm run smoke -- https://web.dev
No API keys, no environment variables, no configuration files.
Why I built this
I have spent years on both sides of this problem: running Google Ads, Meta, and Taboola campaigns, and building the tracking under them myself (GA4, GTM, server-side events). Broken tracking silently burns ad budgets: a duplicate GA4 config doubles every metric, a leftover GTM container re-fires conversions, and nobody notices until the numbers stop making sense. The tools to check for it are scattered across browser extensions, PageSpeed, and manual view-source archaeology.
This server puts the whole check behind one conversational interface, built the way I think an MCP server should be: strict input validation, SSRF-guarded fetching, structured errors instead of crashes, and honest labels on every number (lab vs field, measured vs estimated). The report is written for a developer and a media buyer: which analytics and pixel IDs are installed and where, whether anything double-fires, what the page weighs, and how the response headers look.
Tool reference
All 7 tools take a single url argument (absolute http/https URL) and return Markdown.
| Tool | What it does |
|---|---|
audit_page |
One-shot report: tracking tags, duplicate risks, header hygiene, resource counts. Fast, no browser. |
detect_tracking_tags |
Static HTML scan for GA4, GTM, legacy UA, Google Ads, Meta Pixel, TikTok, LinkedIn Insight, Hotjar, Microsoft Clarity, server-side tagging, and consent platforms. Reports IDs and where each was found. |
find_duplicate_tags |
Flags double-initialized IDs, multiple GTM containers, GTM plus hardcoded GA4 double-fire patterns, duplicate pixel IDs, dead UA tags. |
measure_performance |
Headless-Chromium lab metrics: TTFB, FCP, LCP, CLS, load times, request count, transfer sizes. Clearly labeled lab data. |
analyze_page_weight |
Counts and byte sizes of scripts, styles, images, fonts, and iframes referenced in the HTML; largest third-party scripts. |
check_headers |
Caching, compression, HSTS, CSP, clickjacking, referrer policy. Informational grades with explanations, no synthetic scores. |
tracking_summary |
Plain-language briefing on the tracking setup, written for a media buyer: what is installed, what it means for campaigns, consent posture, issues to raise. |
All example outputs below are real, captured 2026-07-19 against https://web.dev and trimmed for length.
audit_page
The one call to start with: combines the tracking scan, duplicate check, and header check in a single report.
# Audit of https://web.dev/
- HTTP status: 200
- HTML size: 130.7 KB
- Resources referenced in HTML: 14 (0 scripts, 4 stylesheets, 9 images, 0 fonts, 1 iframes; 5 third-party)
[tracking, duplicate, and header sections follow, same content as the
individual tools below, then pointers to the byte-level and lab tools]
detect_tracking_tags
Static scan with evidence: every finding names the vendor, the extracted ID, and the exact place in the HTML that triggered it.
# Tracking tags on https://web.dev/
| Vendor | ID | Found in | Evidence |
|---|---|---|---|
| Google Tag Manager | GTM-MZWCJPP | inline script | `[], "gtm": [{"id": "GTM-MZWCJPP", "purpose": 0}], ...` |
## Distinct IDs
- Google Tag Manager: GTM-MZWCJPP
## Consent signals
- None detected. If this site serves EU users with these tags firing pre-consent,
that is a GDPR/ePrivacy problem.
_Static scan: 6 inline scripts and 0 external script tags inspected. Tags added
at runtime by GTM or a CMP are not visible to this scan._
find_duplicate_tags
Checks for the patterns that double-count: the same GA4 ID initialized twice, multiple GTM containers, a hardcoded GA4 tag next to a GTM container, multiple Meta pixel IDs, the gtag.js loader included more than once, and leftover Universal Analytics tags. web.dev is clean:
# Duplicate-tag check for https://web.dev/
No duplicate IDs or double-fire patterns found across 3 detected tag reference(s).
When something is found, each issue comes with a severity: HIGH (double-counting almost certain), MEDIUM (likely misconfiguration), INFO (verify manually).
measure_performance
Loads the page once in headless Chromium and reports what that load observed. Numbers vary run to run; the report says so.
# Lab performance metrics for https://web.dev/
**LAB DATA**: one headless-Chromium load from this machine. Not field data (no CrUX/RUM), no INP.
| Metric | Value |
|---|---|
| TTFB (time to first byte) | 495 ms |
| FCP (first contentful paint) | 764 ms |
| LCP (largest contentful paint) | 1.02 s |
| CLS (cumulative layout shift) | 0.088 |
| Load event | 2.34 s |
| Requests | 60 |
| Transferred (compressed) | 152.9 KB |
[transfer-by-initiator table and reference thresholds follow]
analyze_page_weight
Static resource inventory with real byte measurements (capped, guarded requests; up to 30 resources, HEAD first, GET fallback):
# Page weight for https://web.dev/
- HTML document: 130.7 KB
- Resources referenced: 14 (0 scripts, 4 stylesheets, 9 images, 0 fonts, 1 iframes)
- Measured 14 of 14 resources: 1.42 MB total
Note the honest gap between this and measure_performance above: the static scan sees 14 referenced resources, the browser load makes 60 requests. JavaScript loads the rest, and each report tells you which view you are looking at.
check_headers
# Header check for https://web.dev/ (HTTP 200)
| Check | Grade | Value | Note |
|---|---|---|---|
| Cache-Control | [OK] | `no-cache, must-revalidate` | Caching policy declared. |
| Compression (Content-Encoding) | [OK] | `gzip` | Response compressed with gzip. |
| HSTS (Strict-Transport-Security) | [OK] | `max-age=63072000; includeSubdomains; preload` | HSTS present with a solid max-age. |
| Referrer-Policy | [MISSING] | n/a | Default referrer behavior leaks full URLs to third parties on some browsers. |
_Grades are informational statements about header presence, not a security score._
tracking_summary
The same detection engine, rendered as a briefing for a non-developer. See the hero screenshot above for the full real output; the sections are: What is installed, What this means for media buying, Consent, Issues to raise.
How it works
src/
index.ts entry point: McpServer + stdio transport, registers the tools
tools.ts tool registration, zod input schemas, Markdown renderers,
error mapping (every failure becomes an isError result)
lib/
url-guard.ts SSRF checks: scheme, hostname denylist, private IP ranges, DNS
safe-fetch.ts hardened fetch: timeout, byte cap, manual redirects, content type
tracking.ts static tag detection (regex + node-html-parser) and duplicate analysis
page-weight.ts resource extraction and capped byte measurement
headers.ts response-header checks
performance.ts Playwright Chromium lab metrics
format.ts byte/ms formatting and grade markers
SSRF protection is layered. url-guard runs two checks: a synchronous one (http/https only; localhost, .local, .internal, .home.arpa and similar hostnames refused; private, loopback, link-local, and CGNAT IP literals refused, for v4 and v6 including IPv4-mapped forms) and an asynchronous one that resolves the hostname with DNS and refuses if any returned address is private, so a public-looking hostname pointing at 192.168.x.x is also blocked. safe-fetch then follows redirects manually (max 5) and re-runs both checks on every hop, enforces a 15 s wall-clock timeout and a 5 MiB body cap, and rejects non-HTML content types for the HTML tools. Every outbound request in the codebase, including page-weight resource measurements, goes through this one client.
Static analysis vs browser analysis. Six of the seven tools parse the served HTML only: fast, no browser dependency, and honest about the boundary (every report footnotes that runtime-injected tags and JS-loaded resources are invisible to it). measure_performance is the one browser tool: it launches headless Chromium via Playwright, registers buffered PerformanceObservers before navigation, loads the page once, waits 3 s for LCP/CLS to settle, and reads the metrics out.
Graceful degradation without Chromium. Playwright is a devDependency and is imported dynamically only inside measure_performance. If the package or the browser binary is missing, the tool returns a structured error containing the exact fix (npx playwright install chromium) instead of crashing, and the other six tools are unaffected.
Tests
npm test runs 60 vitest tests in 6 files, entirely offline (network and DNS are mocked; nothing leaves the process):
url-guard.test.ts: scheme rejection, hostname denylist, private v4/v6 ranges including IPv4-mapped and CGNATsafe-fetch.test.ts: timeouts, byte-cap truncation, manual redirect following and re-validationtracking.test.ts: tag and consent detection plus duplicate analysis against synthetic fixtures (clean.html,duplicates.html,full-stack.html)page-weight.test.ts: resource extraction, third-party heuristic (includingco.uk-style domains), HEAD/GET measurement fallbackheaders.test.ts: grades for present, missing, and weak headerstools.test.ts: a real MCP client calling the real server over the SDK's in-memory transport, covering both success and error paths
npm run smoke -- <url> is the live counterpart: it drives the built stdio server with a real MCP client against a real site, on purpose.
Design decisions
- Never throw from a handler. Every tool wraps its body in try/catch and converts
UrlGuardError,FetchError, andPerformanceUnavailableErrorinto MCPisErrorresults whose message says what to do next. - Reports are Markdown a model can relay verbatim. The renderers produce tables and sections a client model can paste into an answer without rework, and each report ends with an honest footnote about what the scan could not see.
- Bounded everything. 15 s request timeout, 5 MiB response cap, max 5 redirects, 30-resource measurement cap with concurrency 6, 10 s per-resource timeout, content-type enforcement for HTML tools. The numbers live as constants in
safe-fetch.tsandpage-weight.ts. - Honest labels on every number. Lab data is stamped LAB DATA, static scans say they are static, the third-party classifier is documented as a heuristic in the report itself.
- Playwright stays a devDependency so the core install is three runtime dependencies (
@modelcontextprotocol/sdk,node-html-parser,zod) and the browser tool degrades gracefully.
Built with the MCP TypeScript SDK (McpServer + registerTool, zod input schemas, stdio transport). Developed with Claude Code.
Limitations
- Static scan.
detect_tracking_tags,find_duplicate_tags, andtracking_summaryread the served HTML only. Tags injected at runtime by GTM, a CMP after consent, or client-side routing are invisible to them. The reports say so explicitly. - Lab data, not field data.
measure_performanceis one headless load from your machine: no INP (needs real interaction), no CrUX percentiles, and numbers vary run to run. Treat it as a smoke check, not a Core Web Vitals verdict. - Single page scope. Every tool audits exactly the URL you give it. No crawling.
- Third-party classification is a heuristic. Registrable-domain comparison without a full Public Suffix List;
foo.co.ukstyle domains are handled, exotic suffixes may misclassify. - Transfer sizes can read 0 B. Cross-origin resources without
Timing-Allow-Originhide their size from the browser; page-weight measurements reflect what this client was served, which may differ from what your browser gets. - Bot protection. Sites behind aggressive bot walls (Cloudflare challenges and similar) may serve this tool different HTML than they serve a real browser.
License
MIT license, see LICENSE.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。