New:Socket for Asana Is Now Available.Learn more
Get Started

@wcagc/mcp

Package Overview
Dependencies
Maintainers
1
Versions
17
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@wcagc/mcp - npm Package Compare versions

Comparing version
0.2.8
to
0.3.0
+1
-1
dist/server.js

@@ -9,3 +9,3 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

// test/version.test.ts fails the build if the two ever drift.
export const VERSION = "0.2.8";
export const VERSION = "0.3.0";
/**

@@ -12,0 +12,0 @@ * One server, two transports (hosted Streamable HTTP + local stdio) share this — the tool

@@ -59,3 +59,3 @@ import { z } from "zod";

title: "Get a full-site scan run by id",
description: "Polls a run started by scan_site (or a registered-site scan_url). Pro+.",
description: "Polls a full-site run from scan_site, or a scan_url result whose pollWith says get_run. Pro+.",
inputSchema: { runId: z.string().uuid() },

@@ -62,0 +62,0 @@ }, async ({ runId }, extra) => {

import { z } from "zod";
import { apiJson } from "../api-client.js";
import { McpApiError, apiJson } from "../api-client.js";
import { resolveBearer } from "../bearer.js";

@@ -10,26 +10,50 @@ import { toolError } from "../tool-error.js";

function registeredScanText(scan) {
return summarizeScanLike(scan, `Scan for ${scan.requestedUrl}`, "Still in progress — call get_run to poll.");
return `${summarizeScanLike(scan, `Scan for ${scan.requestedUrl}`, "Still in progress — call get_run to poll.")}\n` +
"This URL belongs to a site registered in the account, so the scan is recorded against it — " +
"its findings feed history and trends. Poll it with get_run.";
}
/**
* Falling back is not a failure, so say what happened and what it costs. Without this the caller
* cannot tell a recorded scan from an ad-hoc one, and never learns that registering the site is
* what unlocks history, trends and full-site scans.
*/
function fallbackNote(reason) {
return reason === "unregistered"
? "\nThis URL is not a registered site in the account, so it ran as a one-off scan " +
"(counted against the daily quota). Add the site under Sites to record future scans " +
"against it and unlock history, trends and full-site scans."
: "\nRan as a one-off scan (counted against the daily quota). Recording scans against a " +
"registered site — with history, trends and full-site scans — needs a Pro plan or higher.";
}
/** Codes that mean "this URL cannot use the registered-site path", not "the request was wrong". */
function isNotRegisteredPath(err) {
if (!(err instanceof McpApiError))
return null;
if (err.code === "SITE_NOT_FOUND")
return "unregistered";
if (err.code === "FEATURE_NOT_IN_PLAN" || err.code === "API_KEY_SCOPE_MISSING")
return "plan";
return null;
}
export function registerScanTools(server) {
server.registerTool("scan_url", {
title: "Scan a URL",
description: "Runs a deterministic axe-core accessibility scan of one http(s) URL. The url argument " +
"is ALWAYS required — siteHost never replaces it, it only changes which pipeline the " +
"scan runs through. Omit siteHost for a quick ad-hoc scan of any public URL (free tier, " +
"counted against the daily quota). Add siteHost when that URL belongs to a site " +
"registered in the account, to record the scan against it (Pro+, requires the " +
"sites:read and scans:write scopes) — that unlocks get_run/get_run_findings, full-site " +
"scans, and trends. Either way this queues the scan and returns immediately with an id " +
"to poll; never returns a compliance score — automated testing finds only a portion of " +
"accessibility barriers, see coverageDisclaimer in the result.",
description: "Scans one http(s) URL for accessibility problems with axe-core — deterministic checks, " +
"not a language model's opinion. Pass the URL and nothing else. If it belongs to a site " +
"registered in the caller's account (and their plan allows), the scan is recorded against " +
"that site so it feeds history and trends; otherwise it runs as a one-off. The result says " +
"which happened and which tool to poll with. Returns immediately with an id — never a " +
"compliance score, because automated testing finds only a portion of accessibility " +
"barriers; see coverageDisclaimer in the result.",
inputSchema: {
url: z.string().url().max(2048).describe("The http(s) URL to scan."),
siteHost: z.string().optional().describe("Optional. A registered site's normalized host (Pro+), e.g. \"example.com\" — host only, " +
"no scheme or path. Supplied ALONGSIDE url, never instead of it. Omit for the free " +
"anonymous path."),
},
}, async ({ url, siteHost }, extra) => {
}, async ({ url }, extra) => {
try {
const bearer = resolveBearer(extra);
if (siteHost) {
// Try the registered-site path first: it is strictly the better outcome (the scan is kept,
// and feeds trends). Anything that means "not available for this URL/account" falls back to
// the one-off scan rather than surfacing as an error the caller has to decode and retry —
// deciding that is this server's job, not the assistant's.
try {
const scan = await apiJson(bearer, "/api/v1/scans", {

@@ -39,3 +63,3 @@ method: "POST",

});
const response = withDisclaimer({ scan });
const response = withDisclaimer({ scan, recordedAgainstSite: true, pollWith: "get_run" });
return {

@@ -46,10 +70,16 @@ content: [{ type: "text", text: registeredScanText(scan) }],

}
const response = await apiJson(bearer, "/api/v1/mcp/scan-url", {
method: "POST",
body: JSON.stringify({ url }),
});
return {
content: [{ type: "text", text: freeScanText(response) }],
structuredContent: response,
};
catch (err) {
const reason = isNotRegisteredPath(err);
if (reason === null)
throw err;
const free = await apiJson(bearer, "/api/v1/mcp/scan-url", {
method: "POST",
body: JSON.stringify({ url }),
});
const response = { ...free, recordedAgainstSite: false, pollWith: "get_scan" };
return {
content: [{ type: "text", text: freeScanText(free) + fallbackNote(reason) }],
structuredContent: response,
};
}
}

@@ -62,3 +92,3 @@ catch (err) {

title: "Get a free-tier scan by id",
description: "Polls a scan started by scan_url without siteHost. Returns severity counts, a " +
description: "Polls a one-off scan — use it when scan_url's result says pollWith get_scan. Returns severity counts, a " +
"top-5 finding sample, the coverage disclaimer, and (once terminal) a failure reason if the " +

@@ -82,4 +112,4 @@ "scan did not complete.",

title: "Get a free-tier scan's findings",
description: "The top-5 finding sample for a scan_url (no siteHost) scan (rule id, severity, " +
"help URL, target selector). Full findings for a registered-site scan need get_run_findings.",
description: "The top-5 finding sample for a one-off scan (rule id, severity, help URL, target " +
"selector). For a scan recorded against a registered site, use get_run_findings instead.",
inputSchema: { scanId: z.string().uuid() },

@@ -86,0 +116,0 @@ }, async ({ scanId }, extra) => {

{
"name": "@wcagc/mcp",
"version": "0.2.8",
"version": "0.3.0",
"mcpName": "io.github.WCAG-Compliance/mcp",

@@ -5,0 +5,0 @@ "description": "wcagc MCP server \u2014 a thin, stateless adapter that translates MCP tool calls into wcagc-api HTTP calls. No database, no secrets beyond WCAGC_API_BASE_URL (+ WCAGC_MCP_KEY for local stdio mode). Open-source: this package holds no business logic or credentials of its own.",