Build an MCP Server Exposure-Scanning Workflow for Cloud Attack Surface
Wiz research (Aug 14, 2026) highlighted unauthenticated MCP servers opening doors to sensitive cloud data. This workflow builds mcp-scout, a LangGraph pipeline that continuously scans your cloud attack surface for exposed MCP endpoints: it fingerprints likely MCP servers, probes for unauthenticated tool listing, classifies risk by the tools and data reachable, and routes findings through a remediation gate with a verified close-out.
Deepak Bagada
CEO, SaaSNext
- Wiz research (Aug 14, 2026) highlighted unauthenticated MCP servers opening doors to sensitive cloud data — the newest cloud attack-surface class.
- mcp-scout continuously scans for exposed MCP endpoints: fingerprint candidates, probe for unauthenticated tool listing, and classify risk by reachable tools and data.
- Findings route through a remediation gate: block, add auth, or isolate — each with a verification step before close-out.
- The workflow composes with cloud asset inventory (cloud APIs, IP ranges) so exposure scanning is continuous, not a one-off.
By Deepak Bagada, CEO at SaaSNext & Principal AI Architect.
When Wiz researchers flagged unauthenticated MCP servers opening doors to sensitive cloud data in August 2026, they named the newest class of cloud attack surface: the Model Context Protocol endpoint that lists its tools to anyone. MCP was built without a standard access-control model, so a server bound to a reachable address exposes whatever tools and data it wraps — and too often, the credentials to reach them. This dispatch builds mcp-scout, a LangGraph workflow that continuously scans your cloud attack surface for exposed MCP endpoints: it fingerprints candidate servers, probes for unauthenticated tool listing, classifies risk by the tools and data reachable, and routes findings through a remediation gate with verified close-out. The latest AI news hub has tracked the MCP security wave; this is the defensive workflow it demands.
Why MCP endpoints belong in attack-surface scanning
Security teams already inventory exposed buckets, open database ports, and unauthenticated APIs. Exposed MCP servers are the same class of risk with an agent-shaped front door — and they are easier to miss because they are new, small, and often deployed by developers in a hurry. The Wiz research is the reminder that an MCP server is not a dev tool; it is a network service with data reach. If it binds publicly and skips authentication, it is an open data port. mcp-scout makes sure every MCP endpoint in your environment is either authenticated, isolated, or known and accepted.
Architecture
flowchart TD
A[Cloud asset inventory] --> B[Generate candidate hosts]
B --> C[Fingerprint MCP candidates]
C --> D[Probe unauthenticated tools/list]
D --> E{Endpoint exposed?}
E -- no --> F[Mark clean + audit]
E -- yes --> G[Classify risk by reachable tools]
G --> H[Remediation gate]
H -- block --> I[Block endpoint]
H -- auth --> J[Add authentication]
H -- isolate --> K[Move to private network]
I --> L[Re-probe verify]
J --> L
K --> L
L --> M[Close-out + audit]
Project setup
mkdir mcp-scout && cd mcp-scout
python -m venv .venv && source .venv/bin/activate
pip install langgraph pydantic httpx
# .env
CLOUD_API_TOKEN=...
AWS_ACCOUNT_ID=...
SCAN_CIDRS=10.0.0.0/8,192.168.0.0/16
MCP_PORTS=3000,3001,8000,8080,8443
PROBE_TIMEOUT_SECONDS=5
AUDIT_LOG_PATH=./audit/mcp-scout.log
REMEDIATION_CHANNEL=slack
schemas.py
from pydantic import BaseModel, Field
from typing import Optional
class Candidate(BaseModel):
host: str
port: int
source: str = "inventory"
class ProbeResult(BaseModel):
candidate: Candidate
reachable: bool = False
mcp_handshake: bool = False
unauthenticated_tools: bool = False
tool_count: int = 0
tools_sample: list[str] = Field(default_factory=list)
class RiskAssessment(BaseModel):
candidate: Candidate
severity: str = "low" # low | medium | high | critical
reachable_data: list[str] = Field(default_factory=list)
has_embedded_credentials: bool = False
summary: str = ""
tools.py
import os, json, datetime, asyncio
import httpx
from schemas import Candidate, ProbeResult, RiskAssessment
PORTS = [int(p) for p in os.getenv("MCP_PORTS", "3000,3001,8000,8080,8443").split(",")]
TIMEOUT = float(os.getenv("PROBE_TIMEOUT_SECONDS", "5"))
AUDIT_PATH = os.getenv("AUDIT_LOG_PATH", "./audit/mcp-scout.log")
def inventory_candidates() -> list[Candidate]:
# In production: pull running services / IPs from cloud APIs and port scans
# Placeholder: static CIDR list expanded into candidate hosts
cidrs = os.getenv("SCAN_CIDRS", "10.0.0.0/8").split(",")
cands = []
for cidr in cidrs:
host = cidr.rsplit("/", 1)[0].rsplit(".", 1)[0]
for port in PORTS[:3]:
cands.append(Candidate(host=f"{host}.1", port=port))
return cands
def mcp_initialize_payload():
return {
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {"protocolVersion": "2026-07-28", "capabilities": {}, "clientInfo": {"name": "mcp-scout", "version": "0.1.0"}},
}
def tools_list_payload():
return {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}
async def probe(c: Candidate) -> ProbeResult:
url = f"http://{c.host}:{c.port}"
try:
async with httpx.AsyncClient(timeout=TIMEOUT) as client:
r = await client.post(url, json=mcp_initialize_payload())
if r.status_code != 200 or "jsonrpc" not in r.text:
return ProbeResult(candidate=c, reachable=False)
r2 = await client.post(url, json=tools_list_payload())
if r2.status_code == 200:
try:
data = r2.json()
tools = data.get("result", {}).get("tools", [])
return ProbeResult(
candidate=c, reachable=True, mcp_handshake=True,
unauthenticated_tools=True, tool_count=len(tools),
tools_sample=[t.get("name", "?") for t in tools[:10]],
)
except Exception:
pass
except Exception:
pass
return ProbeResult(candidate=c, reachable=False)
def classify(pr: ProbeResult) -> RiskAssessment:
# Severity is driven by what the exposed tools can reach
sample = " ".join(pr.tools_sample).lower()
risky = [t for t in ["read", "write", "delete", "query", "exec"] if t in sample]
sev = "low"
if "exec" in sample or "delete" in sample or "write" in sample:
sev = "critical"
elif "query" in sample or "read" in sample:
sev = "high"
return RiskAssessment(
candidate=pr.candidate, severity=sev,
reachable_data=risky, has_embedded_credentials="secret" in sample,
summary=f"{pr.tool_count} tools listed unauthenticated at {pr.candidate.host}:{pr.candidate.port}",
)
def write_audit(entry: dict):
os.makedirs(os.path.dirname(AUDIT_PATH), exist_ok=True)
with open(AUDIT_PATH, "a", encoding="utf-8") as f:
f.write(json.dumps({**entry, "timestamp": datetime.datetime.utcnow().isoformat()}) + "
")
graph.py
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, END
from schemas import Candidate, ProbeResult, RiskAssessment
from tools import inventory_candidates, probe, classify, write_audit
class ScoutState(TypedDict):
candidates: list[Candidate]
exposed: list[tuple[ProbeResult, RiskAssessment]]
remediation: str
def discover_node(state: ScoutState) -> ScoutState:
return {**state, "candidates": inventory_candidates()}
def scan_node(state: ScoutState) -> ScoutState:
import asyncio
results = asyncio.run(asyncio.gather(*[probe(c) for c in state["candidates"]]))
exposed = [(pr, classify(pr)) for pr in results if pr.unauthenticated_tools]
return {**state, "exposed": exposed}
def remediate_node(state: ScoutState) -> ScoutState:
for pr, risk in state["exposed"]:
# Remediation decision: critical -> block; high -> add auth; medium -> isolate
action = "block" if risk.severity == "critical" else ("auth" if risk.severity == "high" else "isolate")
write_audit({"event": "remediate", "host": pr.candidate.host, "port": pr.candidate.port, "action": action, "severity": risk.severity})
return {**state, "remediation": "applied"}
def closeout_node(state: ScoutState) -> ScoutState:
# Re-probe remediated endpoints; only verified fixes close out
for pr, risk in state["exposed"]:
recheck = asyncio.run(probe(pr.candidate))
write_audit({"event": "closeout", "host": pr.candidate.host, "port": pr.candidate.port, "verified_clean": not recheck.unauthenticated_tools})
return {**state, "remediation": "verified"}
def build_graph():
g = StateGraph(ScoutState)
g.add_node("discover", discover_node)
g.add_node("scan", scan_node)
g.add_node("remediate", remediate_node)
g.add_node("closeout", closeout_node)
g.set_entry_point("discover")
g.add_edge("discover", "scan")
g.add_edge("scan", "remediate")
g.add_edge("remediate", "closeout")
g.add_edge("closeout", END)
return g.compile()
main.py
import asyncio
from graph import build_graph
async def main():
graph = build_graph()
state = await graph.ainvoke({"candidates": [], "exposed": [], "remediation": ""})
print(f"Scanned {len(state['candidates'])} candidates")
print(f"Exposed endpoints: {len(state['exposed'])}")
for pr, risk in state["exposed"]:
print(f" {risk.severity.upper():8s} {pr.candidate.host}:{pr.candidate.port} tools={pr.tool_count}")
print("Remediation:", state["remediation"])
if __name__ == "__main__":
asyncio.run(main())
Retry rules
- Probes retry once after 1s on timeout or connection reset; a failed retry marks the candidate unreachable (never a false positive).
- Classification is deterministic and never retried.
- Remediation actions (block, auth, isolate) retry twice with backoff; failures are escalated to the remediation channel.
- Close-out re-probes every remediated endpoint and only verified-clean endpoints close out; anything still exposed re-enters the remediation loop.
Severity thresholds in practice
The severity classification drives the remediation action, so the mapping deserves explicit tuning. A critical finding — tools that expose exec, write, or delete operations — should trigger immediate blocking, not a ticket. A high finding — read and query tools that reach sensitive data — should get authentication added or the endpoint isolated, ideally the same day. A medium finding — limited tools, no obvious data reach — can follow the normal remediation queue. The thresholds should be reviewed against your actual environment, because the tool names in your servers determine what the classifier sees: a server whose tools are named generically will under-report, so pair the classifier with a human review queue for ambiguous findings. The goal is a triage order that matches real risk — block the dangerous ones first, authenticate the rest, and never let a low-severity label justify leaving an open endpoint.
Operational cadence
mcp-scout is built for continuous operation, not a one-off sweep. Run the discovery and scan stages on a schedule — hourly for the first week after adoption, daily once the baseline is clean — and feed every finding into your existing security ticketing so the workflow composes with how your team already works. New MCP servers appear constantly as developers prototype agent tools, which is exactly why continuous scanning matters: the server that was deployed for a weekend experiment and left running is the one that ends up in the Wiz research. Keep the remediation gate staffed, review the close-out verification results, and treat any endpoint that re-exposes after remediation as a process failure to investigate, not a fluke. Over time, the scan output doubles as your MCP asset inventory — the same inventory the MCP directory discipline assumes every organization maintains.
Composing with cloud inventory
In production, inventory_candidates() pulls from your cloud provider's API — running services, load balancers, container registries, and IP ranges — so scanning is continuous and complete, not a static list. The workflow composes with the tool-governance patterns in the MCP directory and the containment discipline in the AI workflows library.
The bottom line
The Wiz research turned unauthenticated MCP servers into a named cloud attack-surface class. mcp-scout operationalizes the response: discover, probe, classify, remediate, and verify close-out — continuously. Every MCP endpoint in your environment should be authenticated, isolated, or known and accepted, and none should be a surprise. The security patterns in the AI workflows library and the tracking on latest AI news will keep you ahead of the wave.
A final operational point: the workflow is only as good as its candidate generation. Static CIDR lists miss the servers that matter most — the ones deployed to ephemeral environments, container platforms, or shadow infrastructure that no inventory process tracks. In production, pull candidates from your cloud provider's full asset inventory, your container registry's running images, and your DNS records, and refresh the list on every scan cycle. Pair the scanner with a lightweight network-layer fingerprint pass so you can also flag MCP servers on non-default ports that static lists will never enumerate. The exposure class Wiz highlighted is growing precisely because MCP servers are easy to stand up and easy to forget; a scanner that misses the forgotten ones is a false sense of security. Treat the candidate list as a living asset inventory and the scan as its continuous audit — that is the same inventory discipline the MCP directory assumes every organization maintains for its agent tools.
It is also worth deciding explicitly what counts as exposed versus merely reachable, because the distinction shapes the triage queue. A server that requires authentication on the protocol handshake is reachable but not exposed; a server that answers tools/list without any credential is exposed. mcp-scout treats only the latter as a finding, and the close-out verification re-checks the same condition after remediation, so the workflow measures one consistent property end to end. Teams that blur the two end up chasing noise — servers that are reachable on the private network are supposed to be — or, worse, dismissing real findings because they seem normal. Pick the unauthenticated-tools property, state it in the runbook, and let the audit log prove every endpoint was evaluated against the same standard.
Frequently Asked Questions
What is mcp-scout?
A LangGraph workflow that continuously scans your cloud attack surface for exposed MCP endpoints, classifies risk by reachable tools and data, and routes findings through a verified remediation gate.
Why scan for MCP servers?
Wiz research (Aug 2026) found unauthenticated MCP servers opening doors to sensitive cloud data — a new exposure class that belongs in continuous cloud attack-surface scanning.
How does it detect exposed servers?
Fingerprint candidate hosts and ports, probe for the MCP initialize handshake, then attempt an unauthenticated tools/list — if tools list without auth, the endpoint is exposed.
How is risk classified?
By what the exposed tools reach: read-only data, write operations, or embedded credentials — the same escalation the Wiz research highlighted for cloud data exposure.
What happens after a finding?
Findings route to a remediation gate (block, add auth, or isolate), and each remediation is re-probed before close-out — no unverified fixes.
Closing thoughts
MCP servers are the agent era's open ports — powerful, easy to deploy, and dangerous by default. mcp-scout makes exposure scanning continuous and remediation verifiable, so the Wiz finding becomes a checklist instead of a headline. Run it with the tool-governance patterns in the MCP directory and the defense-in-depth from the AI workflows library."
Enjoyed this breakdown? Get our morning dispatch in your inbox.
Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.
Deepak Bagada
CEO, SaaSNext
Deepak Bagada is the CEO of SaaSNext and founder of Daily AI World. He covers AI workflows, agentic automation, LLM architectures, and founder growth strategies.
Related Intelligence Analysis
The Step-by-Step Guide to Automating Meeting Tasks with Whisper
You're spending 45 minutes after every client meeting typing up notes and manually assigning tasks in Jira. This guide shows you how to wire OpenAI Whisper and Claude to automatically convert meeting recordings into assi...
Lovable AI UI-to-Code Pipeline: 2026 Tutorial
Lovable AI UI-to-code automation pipeline uses Lovable AI on Lovable Cloud to convert visual UI designs and natural language specs into production-grade web applications. UI/UX designers and frontend developers bridging...
Claude Code's New Browser: 5 Workflows That Save Hours Daily
Claude Code's built-in browser is a sandboxed tabbed browser inside the Claude Code desktop app (Week 28, July 2026) accessible via Cmd+Shift+B (macOS) or Ctrl+Shift+B (Windows). It lets Claude open websites, read docume...