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 =', ''));// const zodSchema = eval(zodSchemaStr) as z.ZodType
\x1B[36mat `+r[i].toString()+"\x1B[39m";return n}return t&&(n+=" \x1B[36m"+gy(t)+"\x1B[39m"),n}function gy(e){return T3(I3,e[0])+":"+e[1]+":"+e[2]}function Lp(){var e=Error.stackTraceLimit,t={},r=Error.prepareStackTrace;Error.prepareStackTr …|| ${a} === "boolean" || ${i} === null`).assign(s,(0,te._)`[${i}]`)}}}function fW({gen:e,parentData:t,parentDataProperty:r},n){e.if((0,te._)`${t} !== undefined`,()=>e.assign((0,te._)`${t}[${r}]`,n))}function u0(e,t,r,n=ka.Correct){let i=n== …// const zodSchema = eval(zodSchemaStr) as z.ZodType
// const zodSchema = eval(zodSchemaStr) as z.ZodType
// const zodSchema = eval(zodSchemaStr) as z.ZodType
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.
return await command.exec(this.httpClient);
return await command.exec(this.httpClient);
public async exec(client: Requester): Promise<TResult> {public override async exec(client: Requester): Promise<Documentation[] | string> {public override async exec(client: Requester): Promise<Library[] | string> {import { spawn } from 'node:child_process';const child = spawn('node', [streamableHttpPath, ...args], {}`)(e,Mp,this,t,i);return o}function L3(e,t,r){if(!e||typeof e!="object"&&typeof e!="function")throw new TypeError("argument obj must be object");var n=Object.getOwnPropertyDescriptor(e,t);if(!n)throw new TypeError("must call property on ow …\v\f\r<U+000E><U+000F><U+0010><U+0011><U+0012><U+0013><U+0014><U+0015><U+0016><U+0017><U+0018><U+0019><U+001A>\x1B<U+001C><U+001D><U+001E><U+001F> !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\xA5]^_\`abcdefghijklmnopqrstuvwxy …
`+t.prev}function cd(e,t){var r=fx(e),n=[];if(r){n.length=e.length;for(var i=0;i<e.length;i++)n[i]=bn(e,i)?t(e[i],e):""}var o=typeof cx=="function"?cx(e):[],a;if(Mo){a={};for(var s=0;s<o.length;s++)a["$"+o[s]]=o[s]}for(var c in e)bn(e,c)&&( …`)}function kZ(e){e==null?delete process.env.DEBUG:process.env.DEBUG=e}function bI(){return process.env.DEBUG}function EZ(e){var t,r=process.binding("tty_wrap");switch(r.guessHandleType(e)){case"TTY":t=new xI.WriteStream(e),t._type="tty",t. …`}RI.exports=BZ;function BZ(e,t,r){var n=r||{},i=n.env||process.env.NODE_ENV||"development",o=n.onerror;return function(a){var s,c,u;if(!a&&PI(t)){Zx("cannot 404 after headers sent");return}if(a?(u=JZ(a),u===void 0?u=WZ(t):s=VZ(a),c=HZ(a,u, …`)}function vB(e){e==null?delete process.env.DEBUG:process.env.DEBUG=e}function ZI(){return process.env.DEBUG}function gB(e){var t,r=process.binding("tty_wrap");switch(r.guessHandleType(e)){case"TTY":t=new FI.WriteStream(e),t._type="tty",t. …`)}function j7(e){e==null?delete process.env.DEBUG:process.env.DEBUG=e}function qz(){return process.env.DEBUG}function A7(e){var t,r=process.binding("tty_wrap");switch(r.guessHandleType(e)){case"TTY":t=new Dz.WriteStream(e),t._type="tty",t. …`}function vV(e,t){return t?t instanceof Error?lb(e,t,{expose:!1}):lb(e,t):lb(e)}function gV(e){try{return decodeURIComponent(e)}catch{return-1}}function yV(e){return typeof e.getHeaderNames!="function"?Object.keys(e._headers||{}):e.getHead …Please see the 3.x to 4.x migration guide for details on how to update your app.`)}})};nt.lazyrouter=function(){this._router||(this._router=new HV({caseSensitive:this.enabled("case sensitive routing"),strict:this.enabled("strict routing")}) …|| ${a} === "boolean" || ${i} === null`).assign(s,(0,te._)`[${i}]`)}}}function fW({gen:e,parentData:t,parentDataProperty:r},n){e.if((0,te._)`${t} !== undefined`,()=>e.assign((0,te._)`${t}[${r}]`,n))}function u0(e,t,r,n=ka.Correct){let i=n== …deps: ${r}}`};var RY={keyword:"dependencies",type:"object",schemaType:"object",error:sn.error,code(e){let[t,r]=CY(e);kj(e,t),Ej(e,r)}};function CY({schema:e}){let t={},r={};for(let n in e){if(n==="__proto__")continue;let i=Array.isArray(e[n …`).map(t=>t.trim()).join(" ")};sR.O=function(e){return this.inspectOpts.colors=this.useColors,Bf.inspect(e,this.inspectOpts)}});var uR=g((Bbe,H_)=>{typeof process>"u"||process.type==="renderer"||process.browser===!0||process.__nwjs?H_.expor …`).forEach(function(a){i=a.indexOf(":"),r=a.substring(0,i).trim().toLowerCase(),n=a.substring(i+1).trim(),!(!r||t[r]&&Fre[r])&&(r==="set-cookie"?t[r]?t[r].push(n):t[r]=[n]:t[r]=t[r]?t[r]+", "+n:n)}),t},$R=Symbol("internals");function Wu(e){ …`)}getSetCookie(){return this.get("set-cookie")||[]}get[Symbol.toStringTag](){return"AxiosHeaders"}static from(t){return t instanceof this?t:new this(t)}static concat(t,...r){let n=new this(t);return r.forEach(i=>n.set(i)),n}static accessor …`+e.mark.snippet),n+" "+r):n}function tl(e,t){Error.call(this),this.name="YAMLException",this.reason=e,this.mark=t,this.message=TC(this,!1),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=new Error().stack| …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(csv_filename, "w", newline="") as csvfile:
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:
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.
from urllib.parse import quote
encoded_username = quote(platform_username)
from urllib.parse import urlparse
if urlparse(url).scheme in ("http", "https"):from urllib.parse import quote
encoded_username = quote(platform_username)
import urllib.parse
encoded_params = urllib.parse.urlencode(query_params)
async with httpx.AsyncClient() as client:
async with httpx.AsyncClient() as client:
from urllib.parse import urlparse
if urlparse(command_or_url).scheme in ("http", "https"):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
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": {this.server = new Server(
const server = new Server(
this.server = new Server(
server.registerTool(
server.registerTool(
const server = new Server({mcp = FastMCP("ddg-search", host=_resolved_host, port=_resolved_port)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 →