Running a FastMCP Server
FastMCP turns ordinary Python functions into a Model Context Protocol (MCP) server with a handful of decorators. A server is an instance of FastMCP, and the functions you decorate on it become the tools, resources, and prompts that an MCP client (Claude Desktop, an IDE, an agent framework) can discover and invoke over JSON-RPC.
from fastmcp import FastMCP
mcp = FastMCP("internal-tools")
@mcp.tool
def add(a: int, b: int) -> int:
return a + b
if __name__ == "__main__":
mcp.run()
mcp.run() with no arguments starts the server over the stdio transport: the process talks JSON-RPC on standard input and output and is launched as a subprocess by a single local client. There is no network listener, so reaching the server means already running on the box as that user.
The other transports put the server on the network:
# Network transport: an HTTP listener anyone who can reach the port can drive.
mcp.run(transport="http", host="0.0.0.0", port=8000)
transport="http" (and the older sse) bind a TCP port and speak MCP over HTTP. This is what makes a server shareable between machines, and it is also what converts a local convenience into a remote procedure call surface.
Why running a server matters from an offensive security perspective
The transport is the trust boundary, and the default that “just works” in tutorials is the dangerous one. A stdio server is reachable only by a local process, so its risk is bounded by who already has the box. The moment you switch to transport="http" and bind 0.0.0.0, every decorated function becomes an endpoint reachable by anyone who can route to the port, with no user, no session, and, by default, no authentication. FastMCP supports auth providers but they are opt-in, so a bare mcp.run(transport="http", host="0.0.0.0", port=8000) is an open RPC server.
When you assess an MCP deployment, the first questions are whether there is a network transport at all and what interface it binds. A server on 0.0.0.0 with no auth provider is the headline finding, because the next two pages, FastMCP Tools and FastMCP Resources, are then directly callable by an attacker. The full exploitation of that configuration, enumerating and invoking everything on a no-auth server, is in Unauthenticated FastMCP Servers, with a runnable lab at mcp-fu/fastmcp-no-auth. Treat any network-reachable MCP server as unauthenticated until you have confirmed an auth provider is wired in, and see FastMCP Authentication for what that wiring looks like.
Mitigation
Match the transport to who actually needs to reach the server, and never bind a network listener for a single-client tool. If the server serves one local client, use the default stdio transport so there is no port to attack. If it must be networked, bind to 127.0.0.1 rather than 0.0.0.0 unless remote access is genuinely required, put it behind an authenticating reverse proxy when it is, and always wire in an auth provider (see FastMCP Authentication) so the handshake rejects unauthenticated callers before any tool, resource, or prompt is reachable.
from fastmcp import FastMCP
from fastmcp.server.auth.providers.bearer import BearerAuthProvider
auth = BearerAuthProvider(jwks_uri="https://issuer.example/.well-known/jwks.json",
issuer="https://issuer.example", audience="internal-tools")
mcp = FastMCP("internal-tools", auth=auth)
if __name__ == "__main__":
# Local-only by default; auth required even on loopback.
mcp.run(transport="http", host="127.0.0.1", port=8000)