SkillTotal

Is Klavis MCP server safe?

Some risk - review before installing
Notable — review in context (capabilities are not malware):
  • 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

directory · https://github.com/Klavis-AI/klavis
MEDIUM
30
/ 100 risk score
Snapshot · scanned Sep 24, 2026 · repo@45c9f7d · engine 0.53.0 / ruleset 60

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.

Capabilities — what this component can do (not a risk score):
delegated authenticationdynamic code executionfilesystem readfilesystem writeinstall time executionmcp tools detectednetwork egressshell execution

Behavioral traits

How this component maps to the CSA agentic threat model. Descriptive — it never affects the risk score.

Tool surface
Tool Usage
Execution authority
Tool Access Control / Direct Tool Access
Filesystem reach
Tool Execution Context
Network egress
Interaction & Communication / Direct Communication
Network exposure
Interaction & Communication
Delegated authentication
Tool Execution Context / User Delegated Credentials
Supply-chain provenance risk
General Protections / Supply Chain

Findings (16)

HIGHPossible command injection (shell + dynamic command)ST-CMDI-PY

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.

HIGHNode.js dynamic code executionST-DYN-NODE

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.

HIGHDangerous MCP tool capabilityST-MCP-DANGEROUS-TOOL

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.

HIGHMCP server launches a host commandST-MCP-SERVER-EXEC

An MCP server entry launches a command on your host.

"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.

HIGHNode.js shell/command executionST-SHELL-NODE

The component can run operating-system commands or spawn processes.

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.

HIGHPython shell/command executionST-SHELL-PY

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.

MEDIUMServer bound to all network interfacesST-EXPOSE-BIND

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)
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)
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)
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.

MEDIUMNode.js filesystem readST-FS-NODE-READ

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')
rawSpec = fs.readFileSync(path.resolve(process.cwd(), specPath), 'utf-8')
rawSpec = fs.readFileSync(path.resolve(process.cwd(), specPath), 'utf-8')
rawSpec = fs.readFileSync(path.resolve(process.cwd(), specPath), 'utf-8')

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.

MEDIUMNode.js filesystem write/deleteST-FS-NODE-WRITE

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.writeFile(tempPath, modifiedContent, 'utf-8');
await fs.writeFile(this.memoryFilePath, lines.join("\n"));

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.

MEDIUMPython filesystem readST-FS-PY-READ

The component reads files from disk.

file_content = open(attachment["content"], "rb")
json.loads(open('token.json').read()), SCOPES)
with open(config_file, 'r', encoding='utf-8') 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:

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.

MEDIUMPython filesystem write/deleteST-FS-PY-WRITE

The component writes or deletes files on disk.

with open('token.json', 'w') as token:
with open(output_path, "wb") as output:
with open(requirements_path, 'w') as f:
with open(config_path, 'w') as f:
with open(config_path, 'w') as f:
with open(config_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.

MEDIUMnpm prepare hookST-INSTALL-NPM-PREPARE

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",

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.

MEDIUMNode.js network egressST-NET-NODE

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
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 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);
// Create a fresh axios instance for each request

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.

MEDIUMPython network egressST-NET-PY

The component makes outbound network requests.

async with httpx.AsyncClient() as client:
async with httpx.AsyncClient() as client:
auth = httpx.BasicAuth("", api_key)
async with httpx.AsyncClient() as client:
async with httpx.AsyncClient() as client:
async with aiohttp.ClientSession(headers=headers) as session:
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]
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:
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.

LOWDelegated authentication (OAuth 2.0 / OIDC)ST-AUTH-DELEGATED

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";
"""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)

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.

LOWMCP tool surface detectedST-MCP-DETECTED

An MCP tool surface (manifest or tool definitions) was found.

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-interline-tickets',
server.tool(
    'get-train-route-stations',
const 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 component

How we determine this: deterministic static analysis (regex + AST), evidence-anchored, no code execution. Methodology →