From 64049a944d9aac63e42c3ca2ac81f5a304d06c96 Mon Sep 17 00:00:00 2001 From: New1Direction <285551516+New1Direction@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:37:49 -0600 Subject: [PATCH 1/4] feat(outreach): trust-grade campaign generator + playbooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /api/v1/admin/outreach (x-admin-token) turns the live grade graph into ready-to-send personalized rows (CSV/JSON): - segment=audit → low-graded commercial servers (payments/finance first), each with its real top finding + OWASP code + report URL. - segment=verified → A-tier servers with live percentile + free-badge copy. Copy is written plain — no em-dashes, no AI tells (test asserts it). Feeds an email-finder + cold-email platform; the send runs on the operator's warmed domain. Playbooks: launch/grade-outbound.md (audit, Snyk model — grades stay honest) + launch/verified-pitch.md (A-grade Verified upsell). 3 tests. Co-Authored-By: Claude Opus 4.8 (1M context) --- launch/grade-outbound.md | 96 ++++++++++++++++++++ launch/verified-pitch.md | 84 +++++++++++++++++ worker/src/index.ts | 6 ++ worker/src/outreach.ts | 171 +++++++++++++++++++++++++++++++++++ worker/test/outreach.test.ts | 60 ++++++++++++ 5 files changed, 417 insertions(+) create mode 100644 launch/grade-outbound.md create mode 100644 launch/verified-pitch.md create mode 100644 worker/src/outreach.ts create mode 100644 worker/test/outreach.test.ts diff --git a/launch/grade-outbound.md b/launch/grade-outbound.md new file mode 100644 index 0000000..10a01f7 --- /dev/null +++ b/launch/grade-outbound.md @@ -0,0 +1,96 @@ +# Trust-Grade Outbound — the money engine + +**Play:** you've publicly + independently graded 6,771 MCP servers. **2,580 score D or F.** The *commercial* ones with a public F have a real, urgent problem (anyone evaluating their server sees the grade) and the budget to fix it. Sell them the fix: **Deep Audit** (one-time) + **Continuous Monitoring** ($/mo). Snyk model — find the real problem, sell remediation. + +**The one rule (non-negotiable):** grades stay 100% honest — independent rubric, never fudged to force a sale. That's the moat *and* what makes this legit instead of a shakedown. Within that line it's clean security sales. + +**The CTA mechanic:** every server already has a live public report at `wmcp.sh/mcp/grade/` with the **Deep Audit** + **Watch (Monitoring)** buttons on it. Outreach just surfaces the report + the specific failing finding and points them there. (Confirm the $ shown on the report page before quoting.) + +**Contact channels (you supply):** founder on X / LinkedIn (search " founder"), `security@`/`hello@`, or a GitHub issue if it's a public repo. Funded startups → founder DM converts fastest. + +--- + +## 🔥 Hot leads (real findings, ready to send) + +| Server | Grade | Report | The hook (real finding) | +|---|---|---|---| +| **mcp.viridis-security.com** | F 45 | /mcp/grade/mcp.viridis-security.com | A *security* vendor — its own `detect_injection` tool description contains prompt-injection markup (OWASP MCP01). | +| **api.agentrapay.ai** | F 45 | /mcp/grade/api.agentrapay.ai | Payments — `agentra_authorize_payment` & `create_wallet` flagged for secret-exfiltration surface (MCP08). | +| **crossfin.dev** | F 45 | /mcp/grade/crossfin.dev | Finance — `call_paid_service` / `find_optimal_route` exfiltration surface (MCP08). | +| **mcp.payram.com** | F 45 | /mcp/grade/mcp.payram.com | Payments — connection/env-template tools exfiltration surface (MCP08). | +| **mcp.bitrise.io** | F 45 | /mcp/grade/mcp.bitrise.io | Funded CI co — `register_ssh_key` exfiltration surface (MCP08). | +| **api.dialogbrain.com** | F 45 | /mcp/grade/api.dialogbrain.com | 154 tools; 4+ tool descriptions contain prompt-injection markup (MCP01). | + +> Skip false positives like `sqladmin.googleapis.com` (Google infra, not an operator to sell to). + +--- + +## Outreach templates + +### Cold email (general) +``` +Subject: {host} scored F on the independent MCP trust leaderboard + +Hi {name}, + +I run wmcp.sh — an independent leaderboard that grades MCP servers A–F on +security, spec conformance, reliability, and transparency. It's public and +indexed, so anyone evaluating {company}'s MCP server can see the grade. + +{host} currently scores F (45/100). Public report: {report_url} +Flagged: {finding} (OWASP MCP {code}). + +Two ways to fix it: +• Deep Audit (one-time) — the full breakdown + exactly what to change to pass. +• Continuous Monitoring (/mo) — we re-grade you, prove the fix to your users, + and alert you the moment it regresses. + +The grade is free and identical whether or not you pay — happy to walk you +through the report either way. Want the audit? + +— {you}, wmcp.sh +``` + +### Payments / finance angle (agentrapay, crossfin, payram, payperbyte, merx) +``` +Subject: your payment MCP tool is flagged for a secret-exfiltration surface + +{name} — your {host} MCP server scores F on the independent trust leaderboard, +and the specific flag is the scary one for a payments product: {tool} is marked +for a secret-exfiltration surface (OWASP MCP08). Public report: {report_url}. + +Your customers' security teams will run this check before they connect. The +Deep Audit maps the exact fix; Monitoring re-grades you and proves it's clean. +Worth 15 minutes? +``` + +### Security-vendor angle (viridis-security) +``` +Subject: heads up — viridis-security's MCP server scores F on security + +{name} — friendly heads up from one security-adjacent shop to another. Your +{host} MCP server scores F on the independent MCP trust leaderboard, and the +flag is awkward given what you do: detect_injection's own tool description +contains prompt-injection markup (MCP01). Public report: {report_url}. + +Better you hear it from me than a prospect. Happy to walk you through the audit +(and the badge once you're passing). +``` + +### X / LinkedIn DM (short) +``` +your MCP server {host} scores F on the independent MCP trust leaderboard +(security) — {finding}. public report: {report_url}. it's the first thing a +careful dev checks before connecting. we do the audit + ongoing monitoring if +you want to fix + prove it. (grade's free either way.) +``` + +### Mirror play — the A-graded (sell Verified, easy yes) +1,187 servers score A/A+/A-. DM them: *"your MCP server scored A on the independent trust leaderboard — grab the verified badge to show it on your README/site"* → the Verified SKU. Vanity + trust, low-friction. + +--- + +## Full target list (32 commercial F-graders, finance → dev → other) +crossfin.dev · api.agentrapay.ai · api.dialogbrain.com (154t) · api.delx.ai (143t) · www.ia-qa.com (139t) · mcp.trenchfu.com (94t) · mcp.bitrise.io (81t) · emc2ai.io (69t) · mcp.valuein.biz · merx.exchange · payments.wiselyenterprisesllc.com · www.heista.co · mcp-data.tunnelmind.ai · mcp.payram.com · sats4ai.com · api.butterbase.ai · x711.io · syenite.ai · mcp.realopen.app · mcp.usecoal.xyz · api.octodamus.com · amalgix.io · mcp.frogeye.ai · www.licium.ai · tools.cipherhub.cloud · kapoost.humanmcp.net · mcp.payperbyte.io · qasper.ai · mcp.viridis-security.com + +Each report: `wmcp.sh/mcp/grade/`. Pull a target's exact findings from its report page before you send. diff --git a/launch/verified-pitch.md b/launch/verified-pitch.md new file mode 100644 index 0000000..18a84be --- /dev/null +++ b/launch/verified-pitch.md @@ -0,0 +1,84 @@ +# wmcp.sh Verified — outreach to A-graded MCP servers + +The easy-yes companion to the audit/monitoring outbound (`grade-outbound.md`). Where +F-graders get "here's how to fix it," A-graders get "you earned this — make it +provable." Lower friction, recurring revenue, and every embed is a backlink. + +**Tone bar: independent-auditor professional.** Think Anthropic / Snyk / Stripe, not +growth-hack. Factual, specific, measured. Lead with *their* achievement, not our +product. No superlatives ("revolutionary"), no false urgency, no dark patterns. The +credibility of the grade is the entire asset — the outreach has to sound like it. + +## The offer (accurate — don't overpromise) +- **Free, today:** the live A–F trust badge on every report page (`wmcp.sh/mcp/grade/`). It re-verifies itself, so it shows the *current* grade, not a screenshot. Anyone can embed it. +- **wmcp.sh Verified (paid, recurring):** + - **Claimed ownership** (DNS/meta-tag) + the **Verified mark** — proof the server is really theirs, not a look-alike. + - **Continuous monitoring** — re-audited on a schedule, with an alert the moment anything regresses (a dependency bump, a tool change, a silent rug-pull). Their A stays *true*, and they're never blindsided by a quiet drop. + - A "verified current as of " attestation a static badge can't give. + +The grade is free and independent whether or not they verify — say so plainly. That honesty is the pitch. + +## Scarcity (use the real numbers) +Of 6,771 graded servers: **A+ ≈ 0.2% · A-or-better ≈ 7% · A-tier (incl. A-) ≈ 18%.** Most servers do not pass cleanly. Quote the percentile that matches their grade. + +--- + +## Email — A-graded operator +``` +Subject: {host} passed the independent MCP trust audit (grade {grade}) + +Hi {name}, + +wmcp.sh runs an independent trust audit for MCP servers — security (mapped to the +OWASP MCP Top 10), spec conformance, reliability, tool hygiene, and transparency, +scored A–F and re-checked on a schedule. + +{host} scored {grade}. That's in the top ~{percentile} of the 6,771 servers we've +graded — most don't pass cleanly, so it's worth surfacing to the developers +evaluating whether to connect you. + +You can embed the live trust badge today, free — it re-verifies itself, so it shows +your current grade rather than a screenshot: + {report_url} + +If it's useful, wmcp.sh Verified adds the two things serious operators ask for: + • Claimed ownership (DNS/meta) + the Verified mark — proof it's really your server. + • Continuous monitoring — we re-audit on a schedule and alert you the moment + anything regresses, so your grade stays true and you're never caught out by a + silent drop. + +The grade itself is free and independent — that doesn't change whether you verify. +Report and badge: {report_url}. Glad to answer anything. + +— {name}, wmcp.sh +``` + +## DM — X / LinkedIn (short, same register) +``` +{host} scored {grade} on the independent wmcp.sh MCP trust audit — top ~{percentile} +of 6,771 graded. You can embed the live badge free ({report_url}); Verified adds +claimed ownership + continuous monitoring so it stays provably current. Independent +either way — nice work shipping a clean server. +``` + +## Power-operator note (caseyjhand.com) +One operator runs **12 A+ servers**. Single outreach, highest yield: offer Verified +across the whole fleet (claim once, monitor all) — they clearly care about doing it +right, and 12 verified badges = 12 backlinks. + +--- + +## A-grade targets (commercial, real products) +mcp.gapup.io (A · 271t) · api.domainkits.com (A · 38t) · chat.curie.app (A · 35t) · +mcp.axint.ai (A · 35t) · toolora.dev (A · 34t) · www.cyclesite.co.uk (A · 33t) · +toofi.app (A · 32t) · dynamoi.com (A · 22t) · the caseyjhand.com A+ fleet (12 servers: +usaspending, gbif-biodiversity, secedgar, fcc-broadband, openfda, open-meteo, +clinicaltrials, noaa-cdo, nominatim, pentest, arxiv, cdc). + +Skip cloud infra graded incidentally (compute.googleapis.com, container.googleapis.com) — +not operators to sell to. Pull each target's exact grade + percentile from its report +page before sending. + +## Do / don't (keeps it at the bar) +- **Do** lead with their grade, name the real percentile, link the public report, and state the grade is free + independent. +- **Don't** invent features (no "featured placement" unless it's live), no urgency tricks, no "limited spots," no flattery that isn't backed by the score. diff --git a/worker/src/index.ts b/worker/src/index.ts index 1fc9553..d4abc3a 100644 --- a/worker/src/index.ts +++ b/worker/src/index.ts @@ -1717,6 +1717,12 @@ app.post("/api/v1/admin/seed-now", (c) => runSeedNow(c as any)); app.post("/api/v1/admin/seed-stores", (c) => addSeedStores(c as any)); app.post("/api/v1/admin/seo-indexnow", (c) => submitSeoIndexNow(c as any)); app.post("/api/v1/admin/grade-servers", (c) => addGradeServers(c as any)); +// Outreach campaign generator: turns the live grade graph into ready-to-send +// personalized rows (CSV/JSON) for the audit (F) + verified (A) segments. +app.get("/api/v1/admin/outreach", async (c) => { + const { outreachCampaign } = await import("./outreach"); + return outreachCampaign(c); +}); app.post("/api/v1/admin/regrade-corpus", (c) => regradeCorpus(c as any)); app.post("/api/v1/admin/seed-registry", (c) => seedRegistry(c as any)); app.post("/api/v1/admin/seed-packages", (c) => seedPackages(c as any)); diff --git a/worker/src/outreach.ts b/worker/src/outreach.ts new file mode 100644 index 0000000..220defa --- /dev/null +++ b/worker/src/outreach.ts @@ -0,0 +1,171 @@ +// outreach.ts — admin generator. Turns the live grade graph into a ready-to-send +// outreach campaign: pulls a segment (audit = low-graded commercial servers, +// verified = A-tier commercial servers), fetches each server's real top finding, +// and emits personalized rows (JSON or CSV) to drop into a cold-email platform. +// Regenerates from live grades, so the campaign is always current. +// +// The email copy is written PLAIN on purpose: no em-dashes, no AI tells, short +// human sentences. It has to read like an auditor wrote it, not a model. +// +// GET /api/v1/admin/outreach?segment=audit|verified&max=30&format=csv|json +// header: x-admin-token + +type Env = { CACHE: KVNamespace; ADMIN_TOKEN?: string }; + +const THROWAWAY = /(trycloudflare|vercel\.app|onrender|ngrok|railway\.app|herokuapp|glitch\.me|repl\.co|fly\.dev|run\.app|workers\.dev|supabase\.co|koyeb|googleapis\.com|^npm:|^pypi:|^gh:|localhost)/; +function isCommercial(host: string): boolean { + const parts = host.split("."); + return !THROWAWAY.test(host) && host.includes(".") && parts.length >= 2 && parts.length <= 4; +} +function regDomain(host: string): string { + const p = host.split("."); + return p.length <= 2 ? host : p.slice(-2).join("."); +} +function isPayments(cat: string, host: string, finding: string): boolean { + return /finance|crypto|pay/i.test(cat) || /pay|wallet|crypto|fin|bank|exchange/i.test(host) || /payment|wallet/i.test(finding); +} +function isSecurityVendor(cat: string, host: string): boolean { + return /security/i.test(host) || /security|pentest/i.test(cat); +} + +function topFinding(g: any): { detail: string; owasp: string } { + const fs: any[] = Array.isArray(g?.findings) ? g.findings : []; + const fail = fs.find((f) => f.severity === "fail") || fs.find((f) => f.severity === "warn") || fs[0]; + if (!fail) return { detail: "", owasp: "" }; + return { detail: String(fail.detail || "").replace(/\s+/g, " ").trim(), owasp: String(fail.owasp || "") }; +} + +// Plain copy. No em-dashes, no AI filler. +function auditBody(host: string, grade: string, score: number, finding: string, owasp: string, url: string, cat: string): { subject: string; body: string } { + const payments = isPayments(cat, host, finding); + const sec = isSecurityVendor(cat, host); + let subject = `${host} scored ${grade} on the independent MCP trust audit`; + if (payments) subject = `your payment MCP tool is flagged for a security issue`; + if (sec) subject = `heads up: ${host} scores ${grade} on security`; + const open = sec + ? `Friendly heads up from one security shop to another. Your MCP server ${host} scores ${grade} on the independent wmcp.sh trust audit, and the flag is an awkward one given what you do.` + : payments + ? `Your MCP server ${host} scores ${grade} on the independent wmcp.sh trust audit, and the specific flag is the one a payments product cannot have.` + : `I run wmcp.sh, an independent trust audit for MCP servers. We score them A to F on security (OWASP MCP Top 10), spec conformance, reliability, and transparency, and re-check on a schedule.`; + const body = [ + "Hi,", + "", + open, + "", + `${host} currently scores ${grade} (${score}/100). The flag: ${finding}${owasp ? ` (${owasp}).` : "."}`, + `Full report: ${url}`, + "", + "Anyone evaluating your server sees that grade before they connect. If you want it cleared, the Deep Audit lays out exactly what to change, and Monitoring re-checks you on a schedule and proves the fix to your users.", + "", + "The grade is free and the same whether or not you pay. Happy to walk you through the report either way.", + ].join("\n"); + return { subject, body }; +} + +function verifiedBody(host: string, grade: string, percentile: string, total: number, url: string): { subject: string; body: string } { + const subject = `${host} passed the independent MCP trust audit (grade ${grade})`; + const body = [ + "Hi,", + "", + "wmcp.sh runs an independent trust audit for MCP servers: security (OWASP MCP Top 10), spec conformance, reliability, tool hygiene, and transparency, scored A to F.", + "", + `${host} scored ${grade}, which puts it in roughly the top ${percentile} of the ${total.toLocaleString()} servers we have graded. Most do not pass cleanly, so it is worth showing the developers who are deciding whether to connect you.`, + "", + `You can embed the live badge today for free. It re-checks itself, so it shows your current grade instead of a screenshot: ${url}`, + "", + "If it helps, Verified adds claimed ownership plus continuous monitoring, so the grade stays provably current and you get an alert the moment anything slips.", + "", + `The grade is free and independent either way. Report: ${url}`, + ].join("\n"); + return { subject, body }; +} + +interface Row { + email: string; host: string; domain: string; grade: string; score: number; + category: string; owasp: string; finding: string; report_url: string; + subject: string; body: string; +} + +export async function generateOutreach( + env: Env, segment: "audit" | "verified", max: number, origin: string +): Promise<{ segment: string; total_graded: number; candidates: number; rows: Row[] }> { + // One pass: collect all graded servers' metadata + grade distribution. + const all: Array<{ host: string; grade: string; score: number; cat: string; tc: number }> = []; + const dist: Record = {}; + let cursor: string | undefined, pages = 0; + do { + const list: any = await env.CACHE.list({ prefix: "grade:", limit: 1000, cursor }); + for (const k of list.keys) { + const m = (k.metadata || {}) as any; + if (typeof m.score !== "number") continue; + dist[m.grade] = (dist[m.grade] || 0) + 1; + all.push({ host: k.name.slice("grade:".length), grade: m.grade, score: m.score, cat: m.category || "Other", tc: m.tools_count || 0 }); + } + cursor = list.list_complete ? undefined : list.cursor; pages++; + } while (cursor && pages < 10); + const total = all.length; + + const A = new Set(["A+", "A", "A-"]); + let cands = all.filter((r) => + isCommercial(r.host) && r.tc >= 1 && + (segment === "audit" ? r.score < 60 : A.has(r.grade)) + ); + // audit: worst first, payments/finance prioritized. verified: best first, most tools. + const rank = (g: string) => ({ "A+": 6, A: 5, "A-": 4, "B+": 3, B: 2 } as any)[g] || 1; + if (segment === "audit") cands.sort((a, b) => (isPayments(b.cat, b.host, "") ? 1 : 0) - (isPayments(a.cat, a.host, "") ? 1 : 0) || a.score - b.score || b.tc - a.tc); + else cands.sort((a, b) => rank(b.grade) - rank(a.grade) || b.tc - a.tc); + const candidates = cands.length; + cands = cands.slice(0, Math.min(max, 60)); + + // Percentile per grade from the live distribution (verified copy uses it). + const order = ["A+", "A", "A-", "B+", "B", "B-", "C+", "C", "C-", "D", "F"]; + const atOrAbove = (grade: string) => { + const i = order.indexOf(grade); + let n = 0; + for (let j = 0; j <= i; j++) n += dist[order[j]] || 0; + return total ? Math.max(1, Math.round((n / total) * 100)) : 0; + }; + + const rows: Row[] = []; + for (const c of cands) { + let g: any = null; + try { const raw = await env.CACHE.get(`grade:${c.host}`); if (raw) g = JSON.parse(raw); } catch {} + const { detail, owasp } = topFinding(g); + const url = `${origin}/mcp/grade/${encodeURIComponent(c.host)}`; + const copy = segment === "audit" + ? auditBody(c.host, c.grade, c.score, detail || "security checks failed", owasp, url, c.cat) + : verifiedBody(c.host, c.grade, `${atOrAbove(c.grade)}%`, total, url); + rows.push({ email: "", host: c.host, domain: regDomain(c.host), grade: c.grade, score: c.score, category: c.cat, owasp, finding: detail, report_url: url, subject: copy.subject, body: copy.body }); + } + return { segment, total_graded: total, candidates, rows }; +} + +function csvCell(v: string | number): string { + const s = String(v ?? ""); + return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; +} +function toCsv(rows: Row[]): string { + const cols: (keyof Row)[] = ["email", "host", "domain", "grade", "score", "category", "owasp", "finding", "report_url", "subject", "body"]; + const head = cols.join(","); + const lines = rows.map((r) => cols.map((c) => csvCell(r[c])).join(",")); + return [head, ...lines].join("\n"); +} + +// GET /api/v1/admin/outreach?segment=audit|verified&max=30&format=csv|json +export async function outreachCampaign(c: any): Promise { + const env: Env = c.env; + if (!env.ADMIN_TOKEN || c.req.header("x-admin-token") !== env.ADMIN_TOKEN) { + return c.json({ error: "admin only" }, 401); + } + const segment = c.req.query("segment") === "verified" ? "verified" : "audit"; + const max = Math.min(Math.max(parseInt(c.req.query("max") || "30", 10) || 30, 1), 60); + const format = c.req.query("format") === "csv" ? "csv" : "json"; + const origin = new URL(c.req.url).origin; + const out = await generateOutreach(env, segment, max, origin); + if (format === "csv") { + return new Response(toCsv(out.rows), { + headers: { "content-type": "text/csv; charset=utf-8", "content-disposition": `attachment; filename="outreach-${segment}.csv"` }, + }); + } + return c.json(out); +} diff --git a/worker/test/outreach.test.ts b/worker/test/outreach.test.ts new file mode 100644 index 0000000..3b1e860 --- /dev/null +++ b/worker/test/outreach.test.ts @@ -0,0 +1,60 @@ +// test/outreach.test.ts — the grade-graph → campaign generator. +import { describe, it, expect } from "vitest"; +import { kvMock } from "./helpers"; +import { generateOutreach } from "../src/outreach"; + +const ORIGIN = "https://wmcp.sh"; + +function seed() { + const CACHE = kvMock(); + const put = (host: string, grade: string, score: number, cat: string, tc: number, findings: any[]) => + CACHE.put(`grade:${host}`, JSON.stringify({ grade, score, findings }), { metadata: { grade, score, category: cat, tools_count: tc } }); + put("mcp.payram.com", "F", 45, "Developer Tools", 49, [{ severity: "fail", owasp: "MCP08", detail: "Tool test_payram_connection references sensitive file paths / secrets (exfiltration surface)." }]); + put("api.agentrapay.ai", "F", 45, "Finance & Crypto", 6, [{ severity: "fail", owasp: "MCP08", detail: "Tool agentra_authorize_payment references sensitive file paths / secrets (exfiltration surface)." }]); + put("mcp.gapup.io", "A", 88, "Developer Tools", 271, [{ severity: "info", detail: "No blocking issues." }]); + put("throwaway.vercel.app", "F", 40, "Developer Tools", 5, [{ severity: "fail", owasp: "MCP01", detail: "x" }]); // must be excluded + return { CACHE } as any; +} + +const noDashes = (s: string) => !/[—–]/.test(s); // no em-dash or en-dash + +describe("outreach generator", () => { + it("audit segment: low-grade commercial servers, real finding, no AI dashes", async () => { + const out = await generateOutreach(seed(), "audit", 30, ORIGIN); + expect(out.segment).toBe("audit"); + const hosts = out.rows.map((r) => r.host); + expect(hosts).toContain("mcp.payram.com"); + expect(hosts).toContain("api.agentrapay.ai"); + expect(hosts).not.toContain("throwaway.vercel.app"); // throwaway excluded + expect(hosts).not.toContain("mcp.gapup.io"); // A-grade not in audit + for (const r of out.rows) { + expect(noDashes(r.subject), `dash in subject: ${r.subject}`).toBe(true); + expect(noDashes(r.body), `dash in body: ${r.host}`).toBe(true); + expect(r.body).toContain(r.report_url); + expect(r.report_url).toContain("/mcp/grade/"); + expect(r.finding.length).toBeGreaterThan(0); + } + // payments lead gets the payments-specific framing + const pay = out.rows.find((r) => r.host === "api.agentrapay.ai")!; + expect(pay.subject.toLowerCase()).toContain("payment"); + }); + + it("verified segment: A-tier only, percentile + free-badge copy, no dashes", async () => { + const out = await generateOutreach(seed(), "verified", 30, ORIGIN); + const hosts = out.rows.map((r) => r.host); + expect(hosts).toContain("mcp.gapup.io"); + expect(hosts).not.toContain("mcp.payram.com"); // F not in verified + const g = out.rows.find((r) => r.host === "mcp.gapup.io")!; + expect(noDashes(g.subject)).toBe(true); + expect(noDashes(g.body)).toBe(true); + expect(g.body.toLowerCase()).toContain("top "); // percentile framing + expect(g.body.toLowerCase()).toContain("free"); // free badge hook + expect(g.subject).toContain("passed"); + }); + + it("uses 'A to F' not 'A–F' (dash hygiene on the boilerplate)", async () => { + const out = await generateOutreach(seed(), "verified", 5, ORIGIN); + expect(out.rows[0].body).toContain("A to F"); + expect(out.rows[0].body).not.toContain("A–F"); + }); +}); From 5b59c1055abddc0f0f4057d0db733021f1379b12 Mon Sep 17 00:00:00 2001 From: New1Direction <285551516+New1Direction@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:42:51 -0600 Subject: [PATCH 2/4] feat(outreach): hands-off reply triage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cold-email platform POSTs replies to /api/v1/admin/outreach/reply. We classify and act so the operator barely touches it: optout → auto-suppressed (GET /outreach/suppression feeds the platform) bounce/ooo/cold → logged, never surfaced hot/unknown → ping LEAD_ALERT_WEBHOOK with a pre-drafted, ready-to-send reply Rules-based classifier (no API key), 'not interested' beats the 'interested' substring, copy is dash-free. 13 tests. Co-Authored-By: Claude Opus 4.8 (1M context) --- worker/src/index.ts | 10 +++ worker/src/outreach_reply.ts | 108 +++++++++++++++++++++++++++++ worker/test/outreach_reply.test.ts | 71 +++++++++++++++++++ 3 files changed, 189 insertions(+) create mode 100644 worker/src/outreach_reply.ts create mode 100644 worker/test/outreach_reply.test.ts diff --git a/worker/src/index.ts b/worker/src/index.ts index d4abc3a..1bd3e34 100644 --- a/worker/src/index.ts +++ b/worker/src/index.ts @@ -1723,6 +1723,16 @@ app.get("/api/v1/admin/outreach", async (c) => { const { outreachCampaign } = await import("./outreach"); return outreachCampaign(c); }); +// Reply triage: the cold-email platform POSTs replies here; opt-outs auto-suppress, +// noise is logged, hot leads ping LEAD_ALERT_WEBHOOK with a pre-drafted reply. +app.post("/api/v1/admin/outreach/reply", async (c) => { + const { handleOutreachReply } = await import("./outreach_reply"); + return handleOutreachReply(c); +}); +app.get("/api/v1/admin/outreach/suppression", async (c) => { + const { outreachSuppression } = await import("./outreach_reply"); + return outreachSuppression(c); +}); app.post("/api/v1/admin/regrade-corpus", (c) => regradeCorpus(c as any)); app.post("/api/v1/admin/seed-registry", (c) => seedRegistry(c as any)); app.post("/api/v1/admin/seed-packages", (c) => seedPackages(c as any)); diff --git a/worker/src/outreach_reply.ts b/worker/src/outreach_reply.ts new file mode 100644 index 0000000..922019a --- /dev/null +++ b/worker/src/outreach_reply.ts @@ -0,0 +1,108 @@ +// outreach_reply.ts — hands-off reply triage for the trust-grade campaign. +// +// The cold-email platform (Smartlead/Instantly/AgentMail) POSTs each reply to +// the webhook below. We classify it and act so the operator barely touches it: +// optout → auto-suppress (added to the suppression list, never contacted again) +// bounce → log, ignore +// ooo → log, ignore (auto-reply / out of office) +// cold → log, mark declined +// hot → notify the operator (Slack/webhook) WITH a pre-drafted reply +// unknown → notify too (a real human reply we can't bucket is worth a look) +// +// Only hot/unknown ever surface. Everything else is handled automatically. +// Copy is plain: no em-dashes, no AI tells. +import { fireAlert } from "./alerts"; + +type Env = { KEYS: KVNamespace; LEAD_ALERT_WEBHOOK?: string; ADMIN_TOKEN?: string }; + +// Order of checks matters: bounce, then opt-out, then OOO, then cold (so +// "not interested" beats the "interested" substring), then hot, else unknown. +const BOUNCE = /(mailer-daemon|delivery (status|has )?(failed|failure)|undeliverable|address not found|550 5\.|recipient address rejected|message could not be delivered)/i; +const OPTOUT = /\b(unsubscribe|opt[\s-]?out|remove me|take me off|stop emailing|do ?n['o]?t (contact|email|message) me|do not (contact|email)|leave me alone|lose my (email|address))\b/i; +const OOO = /\b(out of (the )?office|on (vacation|leave|holiday|pto|sabbatical)|away (until|from|this)|annual leave|auto[\s-]?reply|automatic reply|currently unavailable)\b/i; +const COLD = /\b(not interested|no,? thank|no thanks|we'?re (all )?(good|set)|already (have|use|using|got)|we pass|i'?ll pass|not (right now|at this time|for us|a (fit|priority))|no need|please don'?t)\b/i; +const HOT = /\b(interested|how much|pricing|price|cost|tell me more|learn more|send (me )?(the|it|more|over)|book|call|demo|schedule|fix (this|it)|how (do|can) (i|we)|what.*(cost|need|next)|happy to|sounds (good|great)|let'?s (talk|chat|do)|set (up|something)|set a time|grab (15|a|some)|yes\b|sure\b)/i; + +export function classifyReply(subject: string, text: string): { intent: string; reason: string } { + const s = `${subject}\n${text}`; + if (BOUNCE.test(s)) return { intent: "bounce", reason: "delivery failure" }; + if (OPTOUT.test(s)) return { intent: "optout", reason: "unsubscribe / opt-out" }; + if (OOO.test(s)) return { intent: "ooo", reason: "out of office / auto-reply" }; + if (COLD.test(s)) return { intent: "cold", reason: "declined" }; + if (HOT.test(s) || /\?\s*$/.test(text.trim())) return { intent: "hot", reason: "buying signal / question" }; + return { intent: "unknown", reason: "human reply, unclassified" }; +} + +// Pre-drafted response so the operator copies and sends. Plain, no dashes. +function suggestReply(intent: string): string { + if (intent === "cold") { + return [ + "Hi,", + "No problem, thanks for the reply. The grade and report stay free and public regardless, and we keep re-checking the server, so if anything changes you can always grab the audit later. Appreciate the look.", + ].join("\n\n"); + } + return [ + "Hi,", + "Thanks for getting back. Your full report is on the grade page (the link in my last note) with every failing check broken out.", + "The Deep Audit gives you the exact fixes, and Monitoring re-checks you on a schedule so the grade stays current and you are alerted if anything slips. Want me to send the audit link, or is it easier to grab 15 minutes this week?", + ].join("\n\n"); +} + +function authed(c: any): boolean { + const env: Env = c.env; + const token = c.req.query("token") || c.req.header("x-admin-token"); + return !!env.ADMIN_TOKEN && token === env.ADMIN_TOKEN; +} + +// POST /api/v1/admin/outreach/reply?token= +// Body (flexible): { from|email, subject, text|body|message } +export async function handleOutreachReply(c: any): Promise { + if (!authed(c)) return c.json({ error: "unauthorized" }, 401); + const env: Env = c.env; + const b = (await c.req.json().catch(() => null)) || {}; + const from = String(b.from || b.email || b.from_email || b.sender || "").toLowerCase().trim(); + const subject = String(b.subject || ""); + const text = String(b.text || b.body || b.reply_text || b.message || b.content || ""); + if (!from) return c.json({ error: "from_required" }, 400); + + const { intent, reason } = classifyReply(subject, text); + const now = Date.now(); + const rev = (Number.MAX_SAFE_INTEGER - now).toString().padStart(16, "0"); + await env.KEYS.put( + `reply:${rev}`, + JSON.stringify({ from, subject, intent, reason, snippet: text.slice(0, 300), ts: now }), + { expirationTtl: 180 * 86400, metadata: { intent, from } } + ); + + if (intent === "optout") { + await env.KEYS.put(`optout:${from}`, String(now), { metadata: { ts: now } }); + return c.json({ ok: true, intent, action: "suppressed" }); + } + if (intent === "bounce" || intent === "ooo" || intent === "cold") { + return c.json({ ok: true, intent, action: "logged" }); + } + // hot or unknown → surface to the operator with a ready-to-send draft. + const draft = suggestReply(intent); + fireAlert( + env, + c.executionCtx || { waitUntil() {} }, + `HOT reply (${intent}) from ${from}\nsubject: ${subject}\n"${text.slice(0, 220)}"\n\nsuggested reply:\n${draft}` + ); + return c.json({ ok: true, intent, action: "notified", draft }); +} + +// GET /api/v1/admin/outreach/suppression?token= +// The opt-out list, for the send platform to import so nobody is re-contacted. +export async function outreachSuppression(c: any): Promise { + if (!authed(c)) return c.json({ error: "unauthorized" }, 401); + const env: Env = c.env; + const emails: string[] = []; + let cursor: string | undefined, pages = 0; + do { + const r: any = await env.KEYS.list({ prefix: "optout:", limit: 1000, cursor }); + for (const k of r.keys) emails.push(k.name.slice("optout:".length)); + cursor = r.list_complete ? undefined : r.cursor; + pages++; + } while (cursor && pages < 10); + return c.json({ count: emails.length, emails }); +} diff --git a/worker/test/outreach_reply.test.ts b/worker/test/outreach_reply.test.ts new file mode 100644 index 0000000..2164a86 --- /dev/null +++ b/worker/test/outreach_reply.test.ts @@ -0,0 +1,71 @@ +// test/outreach_reply.test.ts — hands-off reply triage. +import { describe, it, expect } from "vitest"; +import { kvMock, envMock, makeCtx } from "./helpers"; +import { classifyReply, handleOutreachReply, outreachSuppression } from "../src/outreach_reply"; + +const noDashes = (s: string) => !/[—–]/.test(s); + +describe("reply classifier", () => { + const cases: Array<[string, string, string]> = [ + ["", "Please unsubscribe me from this list", "optout"], + ["", "take me off your list", "optout"], + ["Re: grade", "How much does the audit cost?", "hot"], + ["", "interested, can you send the link?", "hot"], + ["", "Not interested, we're good thanks", "cold"], // must beat the 'interested' substring + ["Automatic reply", "I'm out of office until Monday", "ooo"], + ["Undeliverable", "Mailer-Daemon: delivery failure, address not found", "bounce"], + ["", "asdf qwer", "unknown"], + ]; + for (const [subj, text, want] of cases) { + it(`'${text.slice(0, 28)}' -> ${want}`, () => { + expect(classifyReply(subj, text).intent).toBe(want); + }); + } +}); + +describe("triage actions (barely-touch automation)", () => { + function ctx(body: any) { + const env = envMock({ KEYS: kvMock(), ADMIN_TOKEN: "t" }); + return { env, c: makeCtx({ env, body, query: { token: "t" } }) }; + } + + it("opt-out auto-suppresses (added to the suppression list)", async () => { + const { env, c } = ctx({ from: "No@Acme.com", subject: "", text: "unsubscribe me please" }); + const res = await handleOutreachReply(c); + expect(res.body.action).toBe("suppressed"); + expect(env.KEYS.__keys("optout:no@acme.com").length).toBe(1); // lowercased + }); + + it("cold / ooo / bounce are logged, not surfaced", async () => { + for (const text of ["not interested", "out of office until July", "delivery failure undeliverable"]) { + const { c } = ctx({ from: "x@y.com", text }); + const res = await handleOutreachReply(c); + expect(res.body.action).toBe("logged"); + } + }); + + it("hot reply is surfaced with a pre-drafted, dash-free reply", async () => { + const { env, c } = ctx({ from: "cto@bitrise.io", subject: "Re: grade", text: "how do we fix this? what does it cost?" }); + const res = await handleOutreachReply(c); + expect(res.body.action).toBe("notified"); + expect(res.body.intent).toBe("hot"); + expect(typeof res.body.draft).toBe("string"); + expect(noDashes(res.body.draft)).toBe(true); + expect(env.KEYS.__keys("reply:").length).toBe(1); // logged + }); + + it("unauthorized without the token", async () => { + const env = envMock({ KEYS: kvMock(), ADMIN_TOKEN: "t" }); + const res = await handleOutreachReply(makeCtx({ env, body: { from: "a@b.com", text: "hi" }, query: {} })); + expect(res.status).toBe(401); + }); + + it("suppression list endpoint returns opted-out emails", async () => { + const env = envMock({ KEYS: kvMock(), ADMIN_TOKEN: "t" }); + await env.KEYS.put("optout:a@b.com", "1"); + await env.KEYS.put("optout:c@d.com", "1"); + const res = await outreachSuppression(makeCtx({ env, query: { token: "t" } })); + expect(res.body.count).toBe(2); + expect(res.body.emails).toContain("a@b.com"); + }); +}); From 6d66f42dea01f63ae1f880baf6c5a38c446a4b14 Mon Sep 17 00:00:00 2001 From: New1Direction <285551516+New1Direction@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:48:49 -0600 Subject: [PATCH 3/4] feat(outreach): route hot leads to email (no chat app required) Hot/unknown replies now go to OPERATOR_EMAIL via Resend (the existing sender), with the message + the pre-drafted reply, so you handle leads from your inbox. Still supports LEAD_ALERT_WEBHOOK; set either or both. Co-Authored-By: Claude Opus 4.8 (1M context) --- worker/src/outreach_reply.ts | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/worker/src/outreach_reply.ts b/worker/src/outreach_reply.ts index 922019a..f2bbe93 100644 --- a/worker/src/outreach_reply.ts +++ b/worker/src/outreach_reply.ts @@ -12,8 +12,16 @@ // Only hot/unknown ever surface. Everything else is handled automatically. // Copy is plain: no em-dashes, no AI tells. import { fireAlert } from "./alerts"; +import { sendEmail, emailEnabled } from "./email"; -type Env = { KEYS: KVNamespace; LEAD_ALERT_WEBHOOK?: string; ADMIN_TOKEN?: string }; +type Env = { + KEYS: KVNamespace; ADMIN_TOKEN?: string; + // Hot leads go to whichever of these is set (you need neither a Slack nor a + // Discord): OPERATOR_EMAIL = your inbox (via Resend), LEAD_ALERT_WEBHOOK = a + // chat webhook. Email is the zero-new-app option. + OPERATOR_EMAIL?: string; LEAD_ALERT_WEBHOOK?: string; + RESEND_API_KEY?: string; RESEND_FROM?: string; +}; // Order of checks matters: bounce, then opt-out, then OOO, then cold (so // "not interested" beats the "interested" substring), then hot, else unknown. @@ -82,12 +90,21 @@ export async function handleOutreachReply(c: any): Promise { return c.json({ ok: true, intent, action: "logged" }); } // hot or unknown → surface to the operator with a ready-to-send draft. + // Goes to email if OPERATOR_EMAIL is set, the chat webhook if that is set, or + // both. No chat app required. const draft = suggestReply(intent); - fireAlert( - env, - c.executionCtx || { waitUntil() {} }, - `HOT reply (${intent}) from ${from}\nsubject: ${subject}\n"${text.slice(0, 220)}"\n\nsuggested reply:\n${draft}` - ); + const ctx = c.executionCtx || { waitUntil() {} }; + if (env.OPERATOR_EMAIL && emailEnabled(env)) { + const esc = (s: string) => String(s).replace(/[&<>]/g, (ch) => ({ "&": "&", "<": "<", ">": ">" } as any)[ch]); + const html = + `

HOT reply (${intent}) from ${esc(from)}

` + + `

Subject: ${esc(subject) || "(none)"}

` + + `
${esc(text.slice(0, 600))}
` + + `

Suggested reply (copy, paste, send):

` + + `
${esc(draft)}
`; + ctx.waitUntil(sendEmail(env, { to: env.OPERATOR_EMAIL, subject: `HOT lead: ${from}`, html }).then(() => {}).catch(() => {})); + } + fireAlert(env, ctx, `HOT reply (${intent}) from ${from}\nsubject: ${subject}\n"${text.slice(0, 220)}"\n\nsuggested reply:\n${draft}`); return c.json({ ok: true, intent, action: "notified", draft }); } From 202705f8723873dab5db28eab00bfe70ba20b2e4 Mon Sep 17 00:00:00 2001 From: New1Direction <285551516+New1Direction@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:56:28 -0600 Subject: [PATCH 4/4] feat(seo): GEO citable stats feed + internal linking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GEO: /api/v1/mcp/stats (+ /mcp/stats.json) — CORS-open, cached JSON of the live trust data with a ready-to-quote citation, so AI answer engines + agents cite wmcp.sh as the source for MCP trust/security. llms.txt now leads with the citable data + the per-server 'is X safe' answer. Internal linking: every grade page now links to its category leaderboard hub + the report, interlinking the 6,771 (previously near-orphaned) grade pages into topical clusters so authority flows. 229 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) --- worker/src/index.ts | 10 ++++++++ worker/src/mcp_grade.ts | 9 +++++++ worker/src/mcp_stats.ts | 41 ++++++++++++++++++++++++++++++ worker/src/u.ts | 10 ++++++++ worker/test/mcp_stats.test.ts | 47 +++++++++++++++++++++++++++++++++++ 5 files changed, 117 insertions(+) create mode 100644 worker/src/mcp_stats.ts create mode 100644 worker/test/mcp_stats.test.ts diff --git a/worker/src/index.ts b/worker/src/index.ts index 1bd3e34..b353710 100644 --- a/worker/src/index.ts +++ b/worker/src/index.ts @@ -499,6 +499,16 @@ app.get("/reports/state-of-mcp-security-2026", async (c) => { }); app.get("/reports/state-of-mcp-security", (c) => c.redirect("/reports/state-of-mcp-security-2026", 301)); app.get("/reports", (c) => c.redirect("/reports/state-of-mcp-security-2026", 302)); +// GEO data surface: machine-readable, citable MCP trust stats for AI answer +// engines + agents (ChatGPT / Claude / Perplexity). CORS-open, hourly-cached. +app.get("/api/v1/mcp/stats", async (c) => { + const { mcpStatsJson } = await import("./mcp_stats"); + return mcpStatsJson(c); +}); +app.get("/mcp/stats.json", async (c) => { + const { mcpStatsJson } = await import("./mcp_stats"); + return mcpStatsJson(c); +}); // Agent-callable MCP trust oracle (grade_mcp_server / check_mcp_drift). Free // read-tier so agents can gate connections on our grade. BEFORE /mcp/:provider. diff --git a/worker/src/mcp_grade.ts b/worker/src/mcp_grade.ts index dab1a42..356e460 100644 --- a/worker/src/mcp_grade.ts +++ b/worker/src/mcp_grade.ts @@ -934,6 +934,15 @@ export function gradePageHtml(r: GradeResult, origin: string): string {
How this grade is computed. An open, independent rubric — Spec conformance (20%), Security mapped to the OWASP MCP Top 10 (30%), Reliability (20%), Tool hygiene (15%), Transparency (15%) — run by connecting to the server and inspecting its real MCP surface. The grade is free and identical whether or not the operator pays. v1 uses static + spec signals from a single connection; continuous uptime, real latency, and annotation-truthing (declared readOnly vs observed behavior) layer on via the wmcp.sh proxy.
+