
Enumerating and Fingerprinting MCP Servers
Week two has been a set of individual attacks: tool poisoning, rug pulls, shadowing, cross server influence, the confused deputy. Today we close the protocol week by turning those into a method. Before you launch any of them against a real target, you profile it. Recon decides which attacks are even worth attempting, and a disciplined profile turns a pile of techniques into an engagement.
This is the Day 3 fingerprinting checklist, grown up. By the end you will have a repeatable recon process and a small script that produces a target profile automatically, the artifact you build first on any MCP assessment, before a single payload.
A hard boundary before we start. Everything here is for servers you own or are explicitly authorized to test, your Day 7 lab, your own deployments, an in scope engagement. Enumeration is still interaction with a system. Do not point it at anyone else’s.
What a profile is for
The point of recon is to answer, cheaply and without firing a payload, which of this week’s attacks apply and how hard each will be. A good profile tells you:
- Where the server sits and how you reach it, which sets the whole stakes
- What it is, so you can map it to known CVEs
- What tools and resources it exposes, so you know your entry points, targets, and exits
- Where auth and scope sit, so you know whether the confused deputy is in play
- What is already suspicious, so you know where to push first
Every item below feeds one of the attacks you already built. Recon is not separate from exploitation. It is exploitation with the safety on.
Step 1: Transport and reachability
First question, always: stdio or HTTP. It changes everything downstream.
stdio means the server runs as a local subprocess of the host. You reach it by influencing what gets installed or configured, not over a network. The stakes are local code execution, because the config file names which subprocesses launch, the exact seam the Cursor config incident used to reach remote code execution. Your recon here is reading config: which servers are registered, from what commands, with what arguments.
HTTP means a network service. Now you have a reachable endpoint, and everything that implies: it can be scanned, its auth tested, its responses probed. Remote servers also carry the SSRF risk that affected a large share of deployed servers, because they fetch URLs. Your recon here is ordinary service recon, plus the MCP specific message probing below.
Write down the transport before anything else. A finding that matters on one is often irrelevant on the other.
Step 2: The handshake, free intelligence
The initialize exchange from Day 3 hands you a fingerprint before you send anything offensive. The server announces its name, version, and capabilities. That maps straight to:
- Known CVEs for that implementation and version, including the RCE rated 9.6 in core MCP infrastructure, CVE-2025-6514, and anything newer
- Which capabilities, and therefore which message types, are even in play
- Clues about who built it and how carefully
This is unauthenticated on many servers. You have learned what you are talking to and you have not touched a single tool. Record the exact server name and version string verbatim, because that string is what you will search advisories against.
Step 3: Enumerate tools, read every description as code
Call tools/list and capture the full output. For each tool, you want four things recorded:
- Name, and whether it collides with any other tool’s name, your shadowing signal from Day 10
- The complete description, read adversarially, the way Day 8 taught, including whitespace regions and comment like text
- The full parameter schema, with every free form string circled as an injection candidate and every parameter that asks for more data than the tool plausibly needs flagged
- A note of which trifecta role it plays: reads untrusted content, reaches valuable data, or sends outward
That last column is the bridge to the Day 4 checklist. By the time you finish enumerating tools, your trifecta table is half filled already.
Two red flags to mark immediately: any description that mentions another tool’s name, which is a cross server influence signal, and any description with content far past where a human would stop reading, which is a hidden instruction signal.
Step 4: Enumerate resources
Call for resources the server exposes and record what they are and, crucially, who can write to them. A resource an outsider can write to is an injection entry point disguised as trusted reference, the RAG and knowledge base poisoning surface. For each resource: what it contains, whether it is ingested into agent context, and the write access around it.
Step 5: Auth and scope probing
This is where you determine whether the confused deputy from Day 11 is live.
- Is there auth on the transport at all, or is the endpoint open
- If there is auth, does it gate individual methods or merely the connection
- What identity does the server act as to its backend, one shared service token or per user credentials
- For OAuth servers, what scopes did consent actually grant versus what the tools need
You often cannot see the backend token directly from outside, but you can infer scope from behavior: if the same tool returns data for any identifier regardless of who asks, the authority is not request bound. That single observation is often the highest value finding in the whole profile, because it is the amplifier on everything else.
Step 6: Automate the profile
Here is a small enumerator you can run against your lab. It connects, captures the handshake and tool list, and flags the suspicious patterns from this week automatically. Treat it as a starting skeleton to extend, not a finished scanner.
# enum.py -- profile an MCP server you own or are authorized to test
import asyncio, json, re
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
SUSPICIOUS = [
(r"ignore (previous|all|prior)", "override instruction"),
(r"do not (mention|tell|report)", "stealth instruction"),
(r"<!--", "comment-wrapped text"),
(r"audit@|@attacker", "exfil address"),
(r"\n\s*\n\s*\n", "large whitespace gap"),
]
def scan_desc(name, desc, all_names):
hits = []
low = (desc or "").lower()
for pat, label in SUSPICIOUS:
if re.search(pat, low):
hits.append(label)
# cross-server / cross-tool reference: description names another tool
for other in all_names:
if other != name and other.lower() in low:
hits.append(f"references other tool '{other}'")
return hits
async def profile(cmd, args):
params = StdioServerParameters(command=cmd, args=args)
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as s:
init = await s.initialize()
print("== SERVER ==")
print(" name/version:", getattr(init, "serverInfo", "n/a"))
listed = await s.list_tools()
names = [t.name for t in listed.tools]
print("\n== TOOLS ==", names)
# name collisions
dupes = {n for n in names if names.count(n) > 1}
if dupes:
print(" [!] NAME COLLISIONS:", dupes)
for t in listed.tools:
print(f"\n-- {t.name}")
schema = t.inputSchema or {}
props = (schema.get("properties") or {})
strings = [k for k, v in props.items()
if v.get("type") == "string"]
print(" string params (injection candidates):", strings)
flags = scan_desc(t.name, t.description, names)
if flags:
print(" [!] FLAGS:", flags)
if __name__ == "__main__":
import sys
asyncio.run(profile(sys.argv[1], sys.argv[2:]))
Run it against your clean Day 7 server, then against the poisoned analytics server from Days 8 to 10:
python enum.py python server.py
python enum.py python analytics_server.py
The clean server reports tools and string parameters. The poisoned one lights up with comment wrapped text, exfil addresses, cross tool references, and whitespace gaps, exactly the signatures of the attacks you built. That is your recon catching your own offense, which is the right way to validate a detector.
Step 7: The target profile artifact
Collapse everything into one profile you would put at the top of an engagement.
TARGET: <server name / endpoint>
Transport: stdio | HTTP Reach: <how>
Implementation: <name> <version> Known CVEs: <list>
Auth: none | connection-only | per-method
Backend identity: shared token | per-user Request-bound: Y/N
TOOLS
<tool> role:[B/A/C] string-params:[…] flags:[…]
…
RESOURCES
<resource> ingested:Y/N external-writable:Y/N
TRIFECTA ROLL-UP
Entry points (B): …
Targets (A): …
Exits ©: …
All three present: Y/N shortest chain: …
PRIORITY FINDINGS (pre-payload)
…
…
Notice this is the Day 6 map and the Day 4 checklist, now produced from recon rather than from a given architecture. You have closed the loop. You can walk up to an unfamiliar MCP server and produce this in an hour, and it tells you which of week two’s attacks to run and in what order.
A note on finding servers at all
In a real engagement the servers are in scope and handed to you. Outside that, the honest guidance is that discovery of other people’s exposed MCP servers, scanning the internet for them, probing endpoints you were not authorized to touch, is not something to do, both legally and ethically. The research showing large fractions of public servers with critical bugs was conducted as security research under appropriate conditions. Your enumeration skills are for targets you are cleared to assess. The method is the same. The authorization is what makes it legitimate.
Detection and defense, the honest version
From the defender’s chair, recon leaves traces and a few things reduce what an attacker learns.
Minimize handshake disclosure. The server need not advertise a precise version string to every client. Reducing fingerprint detail slows CVE mapping, though it is defense in depth, not a real control.
Require auth before tools/list. If enumeration itself requires authentication, an unauthorized party cannot even read your tool descriptions to plan against them. Many servers list tools to anyone, which is the leak that makes recon free.
Run your own enumerator as monitoring. The enum.py pattern, pointed at your own servers on a schedule, is a description and schema drift detector and a poison signature scanner in one. This is the Day 9 pin plus the Day 10 cross reference check, operationalized. Make it part of CI for any server you depend on.
Log enumeration patterns. Repeated tools/list and probing from one client can indicate reconnaissance against an HTTP server. Treat it like any other scanning signal.
The theme holds into recon too. The durable defender posture is to run the attacker’s own profiling against yourself, continuously, so drift and poison show up to you before they show up to anyone else.
Homework for Day 12
- Run
enum.py against your clean Day 7 server and confirm it reports tools and string parameters with no flags.
- Run it against each poisoned variant from Days 8 to 10 and confirm it flags the right signatures: comment wrapped text, exfil address, cross tool reference, whitespace gap, name collision.
- Extend the scanner with one check of your own, for example a parameter whose name suggests bulk data like
profile, dump, all, or context.
- Produce the full target profile artifact for your lab, filled from recon alone, not from your memory of how you built it.
- Produce the same profile for your own real agent’s servers, and for each priority finding note which week two attack it enables.
Point 4 is the week two capstone: a complete, payload free profile of a target, assembled by talking to it the way it expects, that tells you exactly what to attack next.
Week two is done. You can now poison tools, pull rugs, shadow and cross influence servers, exploit the confused deputy, and profile any MCP target into a ranked plan.
Day 13 opens the real world casework: a full breakdown of the postmark-mcp incident, the first confirmed malicious MCP server caught in the wild, mapped stage by stage onto everything you just learned.