Is Klavis MCP server safe?
- Python shell/command execution
- Possible command injection (shell + dynamic command)
- Node.js shell/command execution
repo is an AI directory analyzed by SkillTotal's deterministic static scanner. The scan found no malicious indicators, though 16 risky constructs are reported for review. It can: delegated authentication, dynamic code execution, filesystem read, filesystem write, install time execution, mcp tools detected, network egress and shell execution — capabilities are what the code can do, not a verdict on intent. Risk score 30/100 (medium).
repo
Automated static-analysis result. It can contain false positives and false negatives, and is not a claim about the intent of Klavis MCP server's authors. Report a false positive.
Behavioral traits
How this component maps to the CSA agentic threat model. Descriptive — it never affects the risk score.
Findings (16)
The code builds an OS command out of values that can change at runtime, then runs it through a shell.
return subprocess.run(
command, # command is the full command string in this case
shell=True,
text=True,
capture_output=True,
timeout=self. …Why it matters: If any of those values come from untrusted input, an attacker can run their own commands on the machine.
Fix: Pass arguments as a list without shell=True (e.g. subprocess.run(['git', 'checkout', branch])); never build a shell string from external input. If a shell is unavoidable, quote with shlex.quote.
The code turns strings into live code at runtime (eval / new Function / exec).
const rawData = eval(stationNameJS.replace('var station_names =', ''));Why it matters: If those strings aren't fixed and trusted, they become a way to run arbitrary code.
Fix: Avoid evaluating dynamically constructed code; if unavoidable, ensure the input is a trusted constant and never derived from external data.
An MCP tool exposes a powerful capability (files, shell, network, browser, or credentials).
@mcp.tool() async def storage_download_file(bucket_name: str, source_blob_name: str, destination_file_path: str) -> str:
@mcp.tool()
async def download_attachment(email_id: str, attachment_filename: str) -> str:server.registerTool( "filesystem_read_file",
server.registerTool( "filesystem_read_text_file",
server.registerTool( "filesystem_read_media_file",
server.registerTool( "filesystem_read_multiple_files",
server.registerTool( "filesystem_write_file",
server.registerTool( "filesystem_edit_file",
server.registerTool( "filesystem_create_directory",
server.registerTool( "filesystem_list_directory",
server.registerTool( "filesystem_list_directory_with_sizes",
server.registerTool( "filesystem_directory_tree",
server.registerTool( "filesystem_move_file",
server.registerTool( "filesystem_search_files",
server.registerTool( "filesystem_get_file_info",
server.registerTool( "filesystem_list_allowed_directories",
@mcp.tool()
async def download_attachment(email_id: str, attachment_filename: 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.
An MCP server entry launches a command on your host.
"command": "npx",
"command": "/Users/gongzhe/GitRepos/Office-PowerPoint-MCP-Server/.venv/bin/python",
"command": "D:\\BackDataService\\Office-Word-MCP-Server\\.venv\\Scripts\\python.exe",
"command": "/Users/gongzhe/GitRepos/Office-Word-MCP-Server/.venv/bin/python",
Why it matters: Trusting the manifest means running that binary — verify what it is and where it comes from.
Fix: Verify the launched command and its source before trusting this MCP server configuration.
The component can run operating-system commands or spawn processes.
import { spawn } from 'node:child_process';const child = spawn('node', [streamableHttpPath, ...args], {const childProcess = require('child_process');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; prefer execFile with an argument array.
The component can run operating-system commands or spawn processes.
result = subprocess.run(
[sys.executable, "-m", "pip", "show", "office-powerpoint-mcp-server"],
capture_output=True,
text=True,
check=False
)subprocess.run([sys.executable, '-m', 'venv', venv_path], check=True)
subprocess.run([pip_path, 'install', 'mcp[cli]'], check=True)
subprocess.run([pip_path, 'install', 'python-pptx'], check=True)
subprocess.run([pip_path, 'install', '-r', requirements_path], check=True)
subprocess.run([sys.executable, "-m", "pip", "install", "office-powerpoint-mcp-server"], check=True)
return subprocess.run(
command, # command is the full command string in this case
shell=True,
text=True,
capture_output=True,
timeout=self. …return subprocess.run(
[command] + args,
shell=False,
text=True,
capture_output=True,
timeout=self.security_config.command_timeout, …result = subprocess.run(
[sys.executable, "-m", "pip", "show", "word-document-server"],
capture_output=True,
text=True,
check=False
)subprocess.run([sys.executable, '-m', 'venv', venv_path], check=True)
subprocess.run([pip_path, 'install', 'fastmcp'], check=True)
subprocess.run([pip_path, 'install', 'python-docx'], check=True)
subprocess.run([pip_path, 'install', '-r', requirements_path], check=True)
subprocess.run([sys.executable, "-m", "pip", "install", "word-mcp-server"], check=True)
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60, check=False)
result = subprocess.run(
["code", "--version"], capture_output=True, text=True, timeout=5
)result = subprocess.run(
[target, "--version"], capture_output=True, text=True, timeout=5
)result = subprocess.run(cmd, capture_output=True, text=True)
result = subprocess.run(cmd, capture_output=True, text=True)
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=port)
uvicorn.run(app, host="0.0.0.0", port=8080)
uvicorn.run(app, host="0.0.0.0", port=port)
uvicorn.run(starlette_app, host="0.0.0.0", port=port)
uvicorn.run(starlette_app, host="0.0.0.0", port=port)
uvicorn.run(starlette_app, host="0.0.0.0", port=port)
host="0.0.0.0",
uvicorn.run(starlette_app, host="0.0.0.0", port=port)
uvicorn.run(starlette_app, host="0.0.0.0", port=port)
uvicorn.run(starlette_app, host="0.0.0.0", port=port)
uvicorn.run(starlette_app, host="0.0.0.0", port=port)
uvicorn.run(starlette_app, host="0.0.0.0", port=port)
host="0.0.0.0",
uvicorn.run(starlette_app, host="0.0.0.0", port=port)
host="0.0.0.0",
uvicorn.run(starlette_app, host="0.0.0.0", port=port)
_resolved_host = os.environ.get('HOST') or os.environ.get('FASTMCP_HOST') or "0.0.0.0"uvicorn.run(starlette_app, host="0.0.0.0", port=port)
uvicorn.run(starlette_app, host="0.0.0.0", port=port)
uvicorn.run(starlette_app, host="0.0.0.0", port=port)
uvicorn.run(starlette_app, host="0.0.0.0", port=port)
uvicorn.run(starlette_app, host="0.0.0.0", port=port)
_resolved_host = os.environ.get('HOST') or os.environ.get('FASTMCP_HOST') or "0.0.0.0"uvicorn.run(starlette_app, host="0.0.0.0", port=port)
uvicorn.run(starlette_app, host="0.0.0.0", port=port)
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.
return await fs.readFile(filePath, encoding as BufferEncoding);
const content = normalizeLineEndings(await fs.readFile(filePath, 'utf-8'));
const data = await fs.readFile(this.memoryFilePath, "utf-8");
rawSpec = fs.readFileSync(path.resolve(process.cwd(), specPath), 'utf-8')
const fileStream = fs.createReadStream(filePath)
rawSpec = fs.readFileSync(path.resolve(process.cwd(), specPath), 'utf-8')
const fileStream = fs.createReadStream(filePath)
rawSpec = fs.readFileSync(path.resolve(process.cwd(), specPath), 'utf-8')
const fileStream = fs.createReadStream(filePath)
rawSpec = fs.readFileSync(path.resolve(process.cwd(), specPath), 'utf-8')
const fileStream = fs.createReadStream(filePath)
body: fs.createReadStream(tmpPath)
body: fs.createReadStream(filePath)
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.
await fs.writeFile(filePath, content, { encoding: "utf-8", flag: 'wx' });await fs.writeFile(tempPath, content, 'utf-8');
await fs.unlink(tempPath);
await fs.writeFile(tempPath, modifiedContent, 'utf-8');
await fs.unlink(tempPath);
await fs.writeFile(this.memoryFilePath, lines.join("\n"));await fs.writeFile(tmpPath, captions);
await fs.unlink(tmpPath);
await fs.unlink(outputPath);
await fs.writeFile(outputPath, buffer);
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 reads files from disk.
with open(json_path, 'r') as f:
file_content = open(attachment["content"], "rb")
with open(csv_path, "rb") as source_file:
with open(TOKEN_PATH, 'r') as token:
json.loads(open('token.json').read()), SCOPES)with open(import_file, 'r', encoding='utf-8') as f:
with open(import_file, 'rb') as f:
with open(file_path, 'rb') as f:
with open(config_file, 'r', encoding='utf-8') as f:
with open(config_path, 'r') as f:
with open(template_file_path, 'r', encoding='utf-8') as f:
with open(template_file_path, 'r', encoding='utf-8') as f:
with open(config_path, 'r') as f:
with open(doc_path, 'rb') as f:
with open(metadata_path, 'r') as f:
with open(metadata_path, 'r') as f:
with open(metadata_path, 'r') as f:
with open(metadata_path, 'r') as f:
with open(filename, 'rb') as f:
with open(filename, "rb") as infile:
with open(metadata_path, 'r') as f:
with open(filename, "rb") as infile:
with open(import_file, 'rb') as f:
with open(file_path, 'rb') as f:
with open(config_file, 'r', encoding='utf-8') 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.
with open(TOKEN_PATH, 'w') as token:
with open(TOKEN_PATH, 'w') as token:
with open('token.json', 'w') as token:with open(export_file, 'w', encoding='utf-8') as f:
with open(eml_file, 'wb') as f:
with open(file_path, 'wb') as f:
with open(temp_file_path, 'wb') as f:
os.unlink(temp_file_path)
with open(output_path, "wb") as output:
shutil.rmtree(venv_path)
with open(config_path, 'w') as f:
with open(config_path, 'w') as f:
with open(config_path, 'w') as f:
with open(init_path, 'w') as f:
with open(requirements_path, 'w') as f:
with open(readme_path, 'w') as f:
os.unlink(temp_path)
os.unlink(temp_path)
shutil.rmtree(venv_path)
with open(config_path, 'w') as f:
with open(config_path, 'w') as f:
with open(config_path, 'w') as f:
with open(init_path, 'w') as f:
with open(requirements_path, 'w') as f:
with open(env_example_path, 'w') as f:
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.
package.json has a 'prepare' script (runs on git/local installs and before publishing).
"prepare": "npm run build",
"prepare": "npm run build",
"prepare": "npm run build",
"prepare": "npm run build",
"prepare": "npm run build",
"prepare": "npm run build",
"prepare": "npm run build",
"prepare": "npm run build",
"prepare": "npm run build",
"prepare": "npm run build"
"prepare": "npm run build",
"prepare": "npm run build",
"prepare": "npm run build",
"prepare": "npm run build",
"prepare": "npm run build",
"prepare": "npm run build",
"prepare": "npm run build",
"prepare": "npm run build",
"prepare": "npm run build",
"prepare": "npm run build",
"prepare": "npm run build",
"prepare": "npm run build",
"prepare": "npm run build",
"prepare": "npm run build"
"prepare": "npm run build",
Why it matters: Usually a build step, but confirm it doesn't fetch or run remote code.
Fix: Usually a legitimate build step; confirm it only builds and does not fetch or execute remote code.
The component makes outbound network requests.
import axios from 'axios';
const response = await axios.get(url, {const response = await axios.get(url + '?' + scheme.toString(), {import axios, { AxiosInstance } from "axios";* Create an axios instance with the current access token
return axios.create({if (axios.isAxiosError(error)) {const response = await fetch(url, {const response = await fetch(url, {const webResponse = await fetch(webUrl, {const response = await fetch(url, {const response = await fetch(url, {const response = await fetch(`${baseUrl}/api/v2/skills?${params}`);const response = await fetch(`${baseUrl}/api/v2/skills?${params}`);const response = await fetch(`${baseUrl}/api/v2/skills?${params}`);fetch(`${baseUrl}/api/v2/skills/track`, {const treeResponse = await fetch(treeUrl, {const fileResponse = await fetch(rawUrl);
const response = await fetch(`${authServerUrl}/.well-known/oauth-authorization-server`);const response = await fetch(url, { headers });const response = await fetch(url, { headers });res = await fetch(url, requestOptions as RequestInit);
import axios from "axios";
// Create a fresh axios instance for each request
const axiosInstance = axios.create({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.
The component makes outbound network requests.
async with httpx.AsyncClient() as client:
async with httpx.AsyncClient() as client:
import httpx
auth = httpx.BasicAuth("", api_key)async with httpx.AsyncClient() as client:
async with httpx.AsyncClient() as client:
import aiohttp
async with aiohttp.ClientSession(headers=headers) as session:
import httpx
async with self._semaphore, httpx.AsyncClient() as client: # type: ignore[union-attr]
async with self._semaphore, httpx.AsyncClient() as client: # type: ignore[union-attr]
async with self._semaphore, httpx.AsyncClient() as client: # type: ignore[union-attr]
import httpx
async with httpx.AsyncClient() as client:
async with httpx.AsyncClient() as client:
async with httpx.AsyncClient() as client:
async with httpx.AsyncClient() as client:
import httpx
async with httpx.AsyncClient() as client:
async with httpx.AsyncClient() as client:
async with httpx.AsyncClient() as client:
async with httpx.AsyncClient() as client:
async with httpx.AsyncClient() as client:
async with httpx.AsyncClient() as client:
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 OAuth 2.0 / OpenID Connect delegated-authentication flow was detected (authorization-code / refresh-token / token-exchange grant, an OIDC authorize/discovery endpoint or id_token, or a delegation library). Tools authenticate with the end user's delegated, scoped credentials rather than a long-lived embedded service credential. (9 occurrence(s) shown as evidence).
from google_auth_oauthlib.flow import InstalledAppFlow
dependencies=["google-auth", "google-auth-oauthlib", "google-api-python-client"],
from google_auth_oauthlib.flow import InstalledAppFlow
import { AccessToken, ClientCredentials } from "simple-oauth2";import msal
"""Acquires a token using MSAL and sets it in the context variable for the current request."""
app = msal.ConfidentialClientApplication(client_id, authority=authority, client_credential=client_secret)
import msal
"/v1/oauth/authorize": {Fix: Delegated auth is a lower-blast-radius execution context than an embedded static credential. Confirm the requested scopes are minimal and that tokens are never logged or forwarded off-host.
An MCP tool surface (manifest or tool definitions) was found.
"mcpServers": {"mcpServers": {"tools": [
"mcpServers": {"mcpServers": {"mcpServers": {"mcpServers": {const server = new McpServer({server.tool(
'get-current-date',server.tool(
'get-stations-code-in-city',server.tool(
'get-station-code-of-citys',server.tool(
'get-station-code-by-names',server.tool(
'get-station-by-telecode',server.tool(
'get-tickets',server.tool(
'get-interline-tickets',server.tool(
'get-train-route-stations',this.server = new Server(
const server = new Server(
this.server = new Server(
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 componentHow we determine this: deterministic static analysis (regex + AST), evidence-anchored, no code execution. Methodology →