Documentation
Structural checks free, Basic $5/mo, Pro $19/mo adds the semantic pass.
Overview
Preflight checks an MCP server for real risk signals before an agent connects to it, without a human in the loop. Every check below runs against structure and behavior, not vibes. No check ships until it's been run against known-good and known-bad reference cases with no false positives.
REST API
POST /api/scan
Scan a live server or a manifest directly.
curl -X POST https://preflight.allthepossibles.com/api/scan \
-H "content-type: application/json" \
-d '{"url": "https://example.com/mcp"}'
Body (exactly one of):
| Field | Type | Description |
|---|---|---|
url | string | Live MCP server endpoint (Streamable HTTP transport). Probed with zero credentials. That's the point. |
tools | array | Tool definitions to scan directly: {name, description, inputSchema}[]. Use this when the server isn't reachable over HTTP (e.g. stdio-based). |
serverName | string | Optional label, used in the report and stats. |
Response is a ScanReport: riskScore (0-100), verdict (clear / caution / high-risk), and a findings array, each with severity, category, title, and detail.
GET /api/stats
Aggregate counts across every scan run through Preflight: total scans, verdict breakdown, how many hit unauthenticated transport. Powers the public numbers Preflight cites about itself.
GET /status
Live health check for database connectivity and reachability of the semantic-scan dependency. Returns 200 with {"status": "operational" | "degraded"}.
Content-injection scan
A different check on the same engine: instead of scanning an MCP server's tool list, this scans a document or web page for hidden instructions before an agent reads, summarizes, or acts on it.
POST /api/scan-content
curl -X POST https://preflight.allthepossibles.com/api/scan-content \
-H "content-type: application/json" \
-d '{"content": "the text you want checked"}'
Body (exactly one of):
| Field | Type | Description |
|---|---|---|
content | string | Raw text to scan directly, extracted PDF text, a pasted document, anything already in hand. |
url | string | A page to fetch and scan instead. The raw response body is checked, not a tag-stripped version. Hidden-instruction techniques often live in HTML comments, alt text, or hidden elements, exactly what a tag-strip would discard. |
Response is a ContentScanReport: riskScore, verdict, and a findings array, same severity and category shape as /api/scan. Reuses the same structural checks (hidden Unicode TAG-block characters, obfuscated payloads) and, on Pro, the same kind of semantic pass, with a system prompt written for a document a model is about to read rather than an MCP tool description.
When url is given, this also checks for browser-automation hijack patterns: a high-z-index overlay combined with phrasing that mimics a native browser permission prompt ("wants to show notifications," "verify you're human"), and link text that signals staying in-context ("Continue," "Verify," "Allow") while the href points to a different domain. Same shape of attack the "PleaseFix" disclosure (Zenity Labs, March 2026) documented against browser-automation agents via intent collision, a visible element steering an agent's click somewhere the page's real content never said to go, distinct from the hidden-text techniques above.
VS Code extension and Claude Skill trust check
A different kind of trust check: not an MCP server, something you install or drop into an agent's context directly. A VS Code extension runs with full Node.js access and no permission model to scope in the first place. Nx Console (2.2M installs) was compromised and harvested GitHub/npm/AWS/Vault/Kubernetes/1Password secrets; Red Hat's own npm org was breached the same way. Roughly 90% of detected open-source malware now ships through npm, the same distribution channel a VS Code extension update rides in on.
POST /api/scan-extension, VS Code extension
curl -X POST https://preflight.allthepossibles.com/api/scan-extension \
-H "content-type: application/json" \
-d '{"marketplaceId": "publisher.extension-name"}'
Response includes domainVerified and installCount directly, plus a findings array. One check worth being explicit about: the Marketplace's own "verified" publisher flag turned out, when actually checked against a sample of real extensions, to be present on nearly every publisher including single-person hobbyist accounts, it isn't the meaningful signal it sounds like. This checks isDomainVerified instead, which is rare and means the publisher proved control of a real domain. A high install count from a publisher with no domain verification gets flagged as worth a manual look, not scored as a confirmed risk, since most small legitimate extensions also lack domain verification. The description text also runs through the same known-bad-signature checks as everything else in Preflight.
POST /api/scan-extension, Claude Skill
curl -X POST https://preflight.allthepossibles.com/api/scan-extension \
-H "content-type: application/json" \
-d '{"skillContent": "---\nname: my-skill\ndescription: ...\n---\n\nInstructions..."}'
A Claude Skill is a SKILL.md file: YAML frontmatter (name, description, allowed-tools, disallowed-tools) followed by the Markdown instructions Claude reads and follows, plus optional bundled scripts. Pass the SKILL.md text directly as skillContent, a Worker has no local filesystem to read a path from, and, optionally, scriptContents (an object mapping filename to file content) for any bundled scripts to check alongside it. A skill hiding an instruction in its body is the same problem /api/scan-content already solves, so this reuses that exact engine against the SKILL.md text rather than a separate implementation. Two checks are specific to skills: inline shell execution (a backtick-command placeholder or a fenced \`\`\`! block, which the harness runs once, unattended, the moment the skill loads) and an allowed-tools grant that's unscoped (bare Bash with no command pattern) or references credentials by name. Bundled scripts are checked against the same style of signature matching as everything else in Preflight: environment variables piped to a network call, a known credential file being read, a remote download piped straight into a shell. On Pro, the semantic pass also runs against the skill's body.
Structured-output reliability check
A different failure mode from everything above: not whether something else is safe to trust, but whether your own output is a real answer. Schema validity and answer quality are different guarantees, production data shows even 97% per-step schema compliance compounds to a 74% loop-level success rate at 10 steps, 54% at 20. A JSON blob can parse perfectly and still be a placeholder or a loop that never advanced.
POST /api/check-output
curl -X POST https://preflight.allthepossibles.com/api/check-output \
-H "content-type: application/json" \
-d '{"output": "{\"status\":\"TBD\"}", "expectedFields": ["status"]}'
output is a JSON string or an already-parsed value. expectedFields is optional: top-level field names expected to be present and non-empty. Response is an OutputCheckReport: verdict (looks-real / suspicious / degenerate) and a findings array flagging invalid JSON, a literal placeholder value ("TBD," "N/A," "unknown") where a real answer should be, an array of byte-identical items (a common symptom of a loop that never advanced between iterations), or an expected field that's missing or empty. Pure content analysis, no external calls, so it runs at the same rate limit for everyone rather than gating a heavier tier behind a key.
MCP server
Streamable HTTP, one POST endpoint, four tools.
{
"mcpServers": {
"preflight": { "url": "https://preflight.allthepossibles.com/mcp" }
}
}
scan_mcp_server takes the same input shape as /api/scan and returns the report as JSON text content. scan_content takes the same input shape as /api/scan-content. scan_extension takes the same input shape as /api/scan-extension. check_output takes the same input shape as /api/check-output. All four share the same rate limit.
Findings & scoring
| Check | Status | Cost |
|---|---|---|
| Unauthenticated transport | Live | Free |
| Capability overreach | Live | Free |
| Blind writes (a write or delete tool with no read-only counterpart on the same server) | Live | Free |
| Unbounded destructive arguments (a delete or purge tool whose path, pattern, or filter argument accepts any string, with no enum, pattern, or length cap) | Live | Free |
| Known-bad signatures | Live | Free |
| Templated/cloned deployment (same tool schema, different host) | Live | Free |
| Unenforced authentication (garbage-credential re-probe) | Live | Free |
| Tool description drift (same URL, changed since last scan) | Live | Free |
Content-injection scan (documents and pages, /api/scan-content) | Live | Free |
VS Code extension trust check (publisher domain verification, /api/scan-extension) | Live | Free |
Claude Skill trust check (hidden instructions, tool-scope overreach, bundled-script signatures, /api/scan-extension) | Live | Free (semantic pass Pro only) |
| Browser-automation hijack detection (fake chrome overlays, off-domain trust links) | Live | Free |
| Prompt-injection surface (semantic, Claude-powered) | Live | Pro only |
The unauthenticated-transport check now runs twice when the first attempt is rejected. Once with zero credentials, and, only if that gets a 401/403, once more with a garbage, never-issued bearer token. If the garbage token gets through, the server is checking whether an Authorization header exists, not whether it's valid, and that's flagged as unenforced-authentication rather than being scored as gated. That retry only ever sends a bearer token, so it doesn't exercise a gate built on a session cookie, Basic auth, or mTLS. When the first rejection carries a non-Bearer WWW-Authenticate challenge or a Set-Cookie header, Preflight flags that as non-bearer-primary-auth (severity info) instead of quietly calling it gated, the real enforcement wasn't tested, and that's a different thing from confirming it. If the garbage-string retry is correctly rejected too, there's a third attempt with a syntactically valid but unsigned (alg: none) JWT carrying throwaway claims. A server that decodes JWT structure and trusts the claims inside without verifying the signature will accept this even though it correctly rejected non-JWT-shaped garbage, flagged as forged-token-accepted (critical), a sharper finding than unenforced-authentication because it shows the server will trust any identity a caller chooses to claim.
Tool description drift compares each scan of a URL against the most recent prior scan of that exact URL. Added or removed tools are named directly, and a changed description names the specific tool(s) it changed on too, Preflight stores a hash per tool, not the text itself, so it can tell you which tool moved without telling you what it now says.
Severity weights: critical (34), high (18), medium (9), low (4), info (0), summed and capped at 100. clear is under 12, caution is 12–39, high-risk is 40+.
Capability-overreach findings escalate to critical automatically when the tool's input schema has no enum/pattern constraint on the relevant field. An unscoped grant is worse than a scoped one, and the score reflects that.
Rate limits
20 scans/minute per caller IP on the free tier, tracked in the same database every scan already writes to. A 429 means you've hit it. Back off, don't retry immediately.
| Plan | Price | Rate limit | Semantic prompt-injection check |
|---|---|---|---|
| Free | — | 20/minute per caller IP | No |
| Basic | $5/mo | 150/minute | No |
| Pro | $19/mo | 500/minute | Yes |
Basic raises the rate limit and nothing else: same free-tier checks, same output shape. Pro adds the semantic prompt-injection check on top of the higher limit. Both start with a 7-day free trial at the homepage; managing or canceling either is the same /billing flow. Say something if the free-tier limit is getting in your way.
Known limitations
Ruleset v1 is intentionally small and expands weekly. False negatives on novel attack patterns are expected, not a surprise. If you find one, that's the single most useful thing you can report right now. Latest addition: detection for hidden Unicode TAG-block characters (U+E0000–U+E007F) in tool descriptions, invisible to a human reviewing the tool, readable by an LLM's tokenizer. Credit to mcp-audit for naming this technique with a working example.
Live-URL scanning can't test itself against another Worker on *.workers.dev, or against Preflight's own domain. That's a Cloudflare platform restriction on a zone fetching itself, not a bug, and doesn't affect scanning real third-party servers or pages.
The semantic prompt-injection check only runs with a Pro API key (see the table above). It calls the Claude API per scan, which isn't something to run for free with no billing in front of it.
Preflight reports what a server exposes at connect time, not why it's exposed that way. A deliberately vulnerable server set up as a research honeypot or a decoy looks identical to a genuinely misconfigured one. There's nothing in an MCP handshake that carries intent, and no ruleset change fixes that; it's a structural limit of what a single connection can tell you. The aggregate numbers cited elsewhere on this site (Rapid Claw's audit, Censys's scan) are internet-wide surveys with the same blind spot, not per-server classification, so treat "exposed" as a fact about the connection, not a claim about who put it there.
What Preflight does check: whether this exact tool schema has shown up on other, unrelated hostnames before. Research honeypots are usually templated and deployed at scale, so the same tool set appearing across many otherwise-independent servers is a real (if imperfect; legitimate servers built from the same SDK starter also share this) signal that the connection you're looking at might not be an isolated, organically-neglected deployment. It's informational only and never affects the risk score.
A bare probe that gets back 402 Payment Required is treated as a priced server (x402-style pay-per-call), not as broken or closed, and isn't scored as a risk. What Preflight can't currently tell apart: a server that means to be payable but validates the request body before checking payment, and returns 400 or 405 to a probe with no payload, looks identical from outside to a server that's genuinely broken or closed. That's a real gap, not yet handled, flagged here rather than left implicit.
Live-URL scanning only covers remote, network-reachable MCP servers (Streamable HTTP). Most of the MCP ecosystem isn't that. By one index's count, roughly 71% of registered servers are stdio-based: a local process your client spawns directly, not something with a URL to probe. There's nothing listening on a network for a scanner to talk to, and the risk there isn't "what does this server expose at connect time." It's what the spawned process inherits from your machine (environment variables, filesystem access, egress), which is a different question with a different answer. Preflight doesn't scan stdio servers itself. The tools field on /api/scan lets you run the ruleset (capability overreach, known-bad signatures) against a stdio server's tool definitions if you have them, but that's a manual step, not something Preflight does on its own. It doesn't say anything about what the process itself can reach once spawned.
Why the source isn't here
Preflight's actual code is closed. That's a deliberate call, not an oversight. The value of a security scanner depends partly on not publishing the exact patterns it checks for, the same reasoning every open security tool eventually runs into, from antivirus signatures to WAF rulesets. Publish the detection logic verbatim and you've also published a guide to writing a tool description that slips past it.
What's open instead is everything that doesn't create that risk: this documentation, the methodology above, the aggregate numbers Preflight publishes about itself, and a public repo for issues and discussion. Report a false positive or false negative there. That's the most useful signal we can get, and it doesn't require seeing the ruleset to give it.
Support
Found a false positive, a false negative, or something broken: open an issue. That's the fastest way to reach a real person.
Managing a Pro subscription, invoices, payment method, cancellation: /billing. There's no account login on an API-key product, so it's a form that takes your key and hands you straight to Stripe's own billing portal, nothing is stored beyond validating the key belongs to an active subscription. Checking how many calls a key has made this billing period: /usage, or GET /api/usage with the key as a Bearer token.