SkillTotal

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

directory · https://github.com/Klavis-AI/klavis
MEDIUM
30
/ 100 risk score
Snapshot · scanned Jul 3, 2026 · repo@45c9f7d · engine 0.24.0 / ruleset 25
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

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.

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

Findings (15)

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 =', ''));
// 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== …

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.

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

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.

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

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

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.

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)
encoded_username = quote(platform_username)
from urllib.parse import urlparse
if urlparse(url).scheme in ("http", "https"):
encoded_username = quote(platform_username)
encoded_params = urllib.parse.urlencode(query_params)
async with httpx.AsyncClient() as client:
async with httpx.AsyncClient() as client:
if urlparse(command_or_url).scheme in ("http", "https"):
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:

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.

LOWMCP tool surface detectedST-MCP-DETECTED

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

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 component

Or get notified if this component's risk changes:

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