Is Klavis MCP server safe?
repo is an AI directory analyzed by SkillTotal's deterministic static scanner. The scan found no malicious indicators, though 15 risky constructs are reported for review. It can: 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
- Python shell/command execution
- Possible command injection (shell + dynamic command)
- Node.js shell/command execution
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 Klavis MCP server's authors. Report a false positive.
Findings (15)
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.
const rawData = fs.readFileSync(settingsPath, "utf-8");
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.
fs.unlinkSync(backup.path);
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
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.
import requests
response = requests.post(url, headers=self.headers, json=payload)
response = requests.post(url, headers=self.headers, json=payload)
response = requests.post(url, headers=self.headers, json=payload)
response = requests.post(openai_url, headers=openai_headers, json=payload)
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 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 componentOr get notified if this component's risk changes:
How we determine this: deterministic static analysis (regex + AST), evidence-anchored, no code execution. Methodology →