Is fastmcp safe?
fastmcp is an AI python_package analyzed by SkillTotal's deterministic static scanner. The scan found no malicious indicators, though 7 risky constructs are reported for review. It can: filesystem read, filesystem write, mcp tools detected, network egress and shell execution — capabilities are what the code can do, not a verdict on intent. Risk score 10/100 (low).
fastmcp 3.4.2
- Python shell/command execution
- Dangerous MCP tool capability
- Python network egress
No malicious indicators found by static analysis.
Automated static-analysis result. It can contain false positives and false negatives, and is not a claim about the intent of fastmcp's authors. Report a false positive.
Findings (7)
An MCP tool exposes a powerful capability (files, shell, network, browser, or credentials).
@mcp.tool async def get_access_token_claims() -> dict:
@mcp.tool async def get_access_token_claims() -> dict:
@mcp.tool def read_file(path: str) -> str:
@mcp.tool def take_screenshot() -> Image:
@server.tool(tags={"namespace:admin"})
def reset_user_password(username: str) -> str:@mcp.tool def take_screenshot() -> Image:
@mcp.tool def read_file(path: str) -> str:
Why it matters: Wired into an agent, these grant it real access to your machine — confirm each is required.
Fix: Confirm each powerful tool is required and constrained; broad MCP tools (shell/filesystem/network) grant an agent significant host access.
The component can run operating-system commands or spawn processes.
proc = subprocess.Popen(
[
"uv",
"run",
str(run_with_tracing),
str(echo_path),
"--transport",
"sse",
"--port",
str(ECHO_SERVER_PORT), …process = await asyncio.create_subprocess_exec(
*cmd,
env=env,
start_new_session=sys.platform != "win32",
)subprocess.run([cmd, "--version"], check=True, capture_output=True)
process = subprocess.run(
[npx_cmd, inspector_cmd, *uv_cmd],
check=True,
env=env,
)process = subprocess.run(cmd, check=True, env=env)
process = subprocess.run(cmd, check=True, env=env)
result = subprocess.run(
[claude_in_path, "--version"],
check=True,
capture_output=True,
text=True,
)result = subprocess.run(
[str(path), "--version"],
check=True,
capture_output=True,
text=True,
)subprocess.run(cmd_parts, check=True, capture_output=True, text=True)
subprocess.run(
[gemini_in_path, "--version"],
check=True,
capture_output=True,
)subprocess.run(
[str(path), "--version"],
check=True,
capture_output=True,
)subprocess.run(cmd_parts, check=True, capture_output=True, text=True)
subprocess.run(["open", url], check=True, capture_output=True)
subprocess.run(["xdg-open", url], check=True, capture_output=True)
process = subprocess.run(cmd, check=True)
process = await asyncio.create_subprocess_exec(
*cmd,
stdin=None,
stdout=None,
stderr=None,
# Own process group so _terminate_process can kill the whole tree …subprocess.run(
[
"uv",
"init",
"--project",
str(output_dir),
"--name",
"fastmcp-env",
] …subprocess.run(
[
"uv",
"python",
"pin",
self.python,
"--project",
str(outpu …subprocess.run(
[
"uv",
"add",
*dependencies,
"--no-sync",
"--project",
str(output_dir), …subprocess.run(
[
"uv",
"add",
"-r",
str(req_path),
"--no-sync",
"--project" …subprocess.run(
[
"uv",
"add",
"--editable",
*editable_paths,
"--no-sync",
" …subprocess.run(
["uv", "sync", "--project", str(output_dir)],
check=True,
capture_output=True,
text=True,
)r = subprocess.run(
[sys.executable, "-c", code],
capture_output=True,
text=True,
timeout=30,
)Why it matters: Powerful and often legitimate — confirm the commands aren't built from untrusted input.
Fix: Confirm the command and its arguments are fully controlled and not derived from untrusted input; avoid shell=True.
A server is bound to all network interfaces (0.0.0.0), not just your own machine.
uvicorn.run(app, host="0.0.0.0", port=8000)
create_server().run(transport="sse", host="0.0.0.0", port=8001, path="/sse")
examples=["127.0.0.1", "0.0.0.0", "localhost"],
Why it matters: Without authentication, other hosts on the network can reach it.
Fix: Bind to 127.0.0.1 for local-only use, or require authentication and restrict access if remote exposure is intended.
The component reads files from disk.
with open(path) as f:
with open(path) as f:
app_bridge_js = app_bridge_cache.read_text(encoding="utf-8")
with open(Path(server_spec)) as f:
data = json.loads(path.read_text())
text = path.read_text()
text = path.read_text()
text = path.read_text()
content = config_file.read_text().strip()
with open(config_path) as f:
with open(config_path) as f:
content := file_path.read_text(encoding="utf-8").strip()
content: str | bytes = await self._async_path.read_bytes()
content = await self._async_path.read_text(encoding=self.encoding)
with open(path, "rb") as f:
return main_file_path.read_text(encoding="utf-8")
return full_path.read_text(encoding="utf-8")
return full_path.read_bytes()
return full_path.read_text(encoding="utf-8")
return full_path.read_bytes()
content = main_file.read_text(encoding="utf-8")
with open(config_path) as f:
with open(self.path, "rb") as f:
with open(self.path, "rb") as f:
with open(self.path, "rb") as f:
Why it matters: Usually legitimate, but worth confirming it can't be steered into reading sensitive files.
Fix: Confirm which files are read and that paths cannot be influenced by untrusted input to reach sensitive locations.
The component writes or deletes files on disk.
app_bridge_cache.write_text(app_bridge_js, encoding="utf-8")
with open(output_path, "w") as f:
output_path.write_text(script)
skill_path.write_text(skill_content)
config_file.write_text('{"mcpServers": {}}')file_path.write_text(self.model_dump_json(indent=2), encoding="utf-8")
with open(output, "w") as f:
file_path.write_text(content.text)
file_path.write_bytes(base64.b64decode(content.blob))
cache_path.write_text(
json.dumps({"latest_version": latest_version, "timestamp": time.time()})
)Why it matters: Usually legitimate, but worth confirming the paths can't be controlled by untrusted input.
Fix: Confirm which files are written/deleted and that paths cannot be influenced by untrusted input.
The component makes outbound network requests.
import httpx
resp = httpx.get(
NOMINATIM_URL,
params={"q": query, "format": "json", "limit": 1},
headers={"User-Agent": "fastmcp-map-example/1.0"},
timeout=10,
)response = httpx.get(url, follow_redirects=True)
response = httpx.get(url, follow_redirects=True)
import httpx
async with httpx.AsyncClient() as client:
import aiohttp
async with aiohttp.ClientSession() as session:
from urllib.parse import urlparse
if urlparse(str(r.uri)).netloc not in ("weather", "news")str(r.uri) for r in resources if urlparse(str(r.uri)).netloc == "weather"
str(r.uri) for r in resources if urlparse(str(r.uri)).netloc == "news"
import httpx
with httpx.Client() as client:
from urllib.parse import urlparse
parsed_url = urlparse(args.url)
from urllib.parse import quote
import httpx
with httpx.Client(timeout=30.0) as client:
with httpx.Client(timeout=30.0) as client:
with httpx.Client(timeout=30.0) as client:
args_json = quote(json.dumps(tool_args))
client = httpx.AsyncClient(
timeout=httpx.Timeout(60.0, read=None), trust_env=False
)Why it matters: Usually legitimate, but confirm the destinations are expected and no sensitive data leaves.
Fix: Confirm the destination hosts are expected and that no sensitive data is sent off-host.
An MCP tool surface (manifest or tool definitions) was found.
mcp = FastMCP()
mcp = FastMCP("smoke")assert FastMCP("smoke").name == "smoke"Why it matters: Just context — review which tools it offers and their permissions.
Fix: Review the declared MCP tools and their permissions.
Check your own component
Run the same evidence-backed scan on any MCP server, agent skill, or package.
Scan your own componentOr get notified if this component's risk changes:
How we determine this: deterministic static analysis (regex + AST), evidence-anchored, no code execution. Methodology →