SkillTotal

Is Attio MCP server safe?

Some risk - review before installing
Notable — review in context (capabilities are not malware):
  • Node.js shell/command execution
  • Possible command injection (exec with dynamic command)
  • Remote pipe-to-shell execution

attio-mcp is an AI npm_package analyzed by SkillTotal's deterministic static scanner. The scan found no malicious indicators, though 12 risky constructs are reported for review. It can: delegated authentication, 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 40/100 (medium).

attio-mcp 1.6.1

npm_package · https://github.com/kesslerio/attio-mcp-server
MEDIUM
40
/ 100 risk score
Snapshot · scanned Aug 5, 2026 · attio-mcp@1.6.1 · engine 0.38.1 / ruleset 42

Automated static-analysis result. It can contain false positives and false negatives, and is not a claim about the intent of Attio MCP server's authors. Report a false positive.

Capabilities — what this component can do (not a risk score):
delegated authenticationfilesystem 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
Delegated authentication
Tool Execution Context / User Delegated Credentials
Supply-chain provenance risk
General Protections / Supply Chain

Findings (12)

HIGHPossible command injection (exec with dynamic command)ST-CMDI-NODE

The code builds an OS command out of values that can change at runtime, then runs it through a shell.

const count = execSync(`find . -name "*${ext}" -not -path "./node_modules/*" -not -path "./dist/*" | wc -l`, { encoding: 'utf8' }).trim();
const result = execSync(`npm run test:${suite} 2>/dev/null || echo "FAILED"`, { encoding: 'utf8' });
const diffOutput = execSync(
        `git diff --name-status ${baseBranch}...HEAD`,

Why it matters: If any of those values come from untrusted input, an attacker can run their own commands on the machine.

Fix: Use execFile/spawn with an argument array instead of exec; never build a shell command string from external input.

HIGHnpm install-time lifecycle hookST-INSTALL-NPM

package.json runs scripts automatically when the package is installed.

"postinstall": "[ -d .git ] && husky || echo 'Skipping git hooks setup (not a git repository)'",

Why it matters: Install scripts are a favorite supply-chain foothold — they execute on every machine that installs the package.

Fix: Inspect the hook command. Install-time scripts are a common supply chain execution vector; ensure they do nothing beyond a documented build step.

HIGHNode.js shell/command executionST-SHELL-NODE

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

const { execSync } = require('child_process');
const output = execSync(command, {
return execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();
const { execFileSync } = require('node:child_process');
import { execFileSync } from 'node:child_process';
while ((match = pattern.exec(source)) !== null) {
while ((match = regex.exec(source)) !== null) {
while ((match = regex.exec(source)) !== null) {
import { execSync } from 'child_process';
import { spawn } from 'child_process';
const child = spawn('node', ['dist/index.js'], {
const { execSync } = require('child_process');
const count = execSync(`find . -name "*${ext}" -not -path "./node_modules/*" -not -path "./dist/*" | wc -l`, { encoding: 'utf8' }).trim();
const srcLines = execSync('find src -name "*.ts" | xargs wc -l | tail -1', { encoding: 'utf8' }).trim().split(/\\s+/)[0];
const testLines = execSync('find test -name "*.ts" | xargs wc -l | tail -1', { encoding: 'utf8' }).trim().split(/\\s+/)[0];
const result = execSync(`npm run test:${suite} 2>/dev/null || echo "FAILED"`, { encoding: 'utf8' });
const lintResult = execSync('npm run lint:check 2>&1 || echo "LINT_FAILED"', { encoding: 'utf8' });
const tscResult = execSync('npm run typecheck 2>&1 || echo "TSC_FAILED"', { encoding: 'utf8' });
const formatResult = execSync('npm run check:format 2>&1 || echo "FORMAT_FAILED"', { encoding: 'utf8' });
const commits = execSync('git log --oneline -10', { encoding: 'utf8' })
const contributors = execSync('git shortlog -sn --all', { encoding: 'utf8' })
const branches = execSync('git branch -r', { encoding: 'utf8' })
return execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();

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.

HIGHRemote pipe-to-shell executionST-SHELL-PIPE-EXEC

A remotely fetched payload is piped straight into a shell (e.g. `curl … | bash`). The component runs code downloaded at runtime, which is unreviewable and a common second-stage delivery vector. (5 occurrence(s) shown as evidence).

curl -LsSf https://astral.sh/uv/install.sh | sh

Fix: Download to a file, inspect it, and run a pinned/verified copy instead of piping a network response directly into a shell.

MEDIUMNode.js filesystem readST-FS-NODE-READ

The component reads files from disk.

const raw = fs.readFileSync(file, 'utf8');
content = fs.readFileSync(envPath, 'utf-8');
const content = fs.readFileSync(envPath, 'utf-8');
return JSON.parse(fs.readFileSync(this.budgetsPath, 'utf8'));
const latest = JSON.parse(fs.readFileSync(
const baseline = JSON.parse(fs.readFileSync(
const content = fs.readFileSync(fullPath, 'utf8');
const content = fs.readFileSync(fullPath, 'utf8');
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
stats.coverage = JSON.parse(fs.readFileSync('coverage/coverage-summary.json', 'utf8'));
stats.budgets = JSON.parse(fs.readFileSync(budgetsPath, 'utf8'));
stats.latest = JSON.parse(fs.readFileSync(
return JSON.parse(fs.readFileSync(
let content = fs.readFileSync(filePath, 'utf8');
let content = fs.readFileSync(filePath, 'utf8');
let content = fs.readFileSync(filePath, 'utf8');
let content = fs.readFileSync(filePath, 'utf8');
const changelogContent = fs.readFileSync(changelogPath, 'utf8');
const indexContent = fs.readFileSync('./src/objects/people/index.ts', 'utf8');
const envContent = fs.readFileSync(envPath, 'utf8');
fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf8')
const templateSource = await fs.readFile(templatePath, 'utf8');
const content = fs.readFileSync(filePath, 'utf8');
fs.readFileSync(CONFIG_PATHS.user, 'utf8')
fs.readFileSync(CONFIG_PATHS.user, 'utf8')

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(file, JSON.stringify(evaluation, null, 2), 'utf8');
fs.writeFileSync(envPath, content.trim() + '\n');
fs.writeFileSync(filepath, JSON.stringify(data, null, 2));
fs.writeFileSync(reportPath, report);
fs.writeFileSync(protectedFile, analysis.protected.join('\\n') + '\\n');
fs.writeFileSync(deleteFile, analysis.toDelete.join('\\n') + '\\n');
fs.writeFileSync(ambiguousFile, analysis.ambiguous.join('\\n') + '\\n');
fs.writeFileSync(reportFile, report);
fs.writeFileSync(filepath, content);
fs.writeFileSync(filepath, content);
fs.unlinkSync(latestPath);
fs.writeFileSync(destination, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
fs.writeFileSync(filePath, content);
fs.writeFileSync(filePath, content);
fs.writeFileSync(filePath, content);
fs.writeFileSync(filePath, content);
fs.unlinkSync(sourcePath);
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
fs.writeFileSync(htmlReportPath, html);
fs.writeFileSync(path.resolve(process.cwd(), outputPath), releaseNotes);
fs.writeFileSync(reportPath, report);
await fs.writeFile(fullPath, content, 'utf8');
fs.writeFileSync(jsonPath, JSON.stringify(report, null, 2), 'utf8');

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.

with open(log_file, 'r') as f:
with open(log_file, 'r') as f:
with open(log_file, 'r') as f:
with open(history_file, 'r') as f:
with open(args.workspace_schema_file) 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(history_file, 'w') as f:
with open(output_file, 'w') as f:
os.unlink(log_files[0])
(skill_dir / 'SKILL.md').write_text(skill_content)
(skill_dir / 'resources' / 'workflows.md').write_text(workflows_content)
(skill_dir / 'resources' / 'tool-reference.md').write_text(tool_ref_content)
(skill_dir / 'resources' / 'examples.md').write_text(examples_content)
(skill_dir / 'SKILL.md').write_text(skill_md_content)
(skill_dir / 'resources' / 'README.md').write_text(resources_readme)
(skill_dir / 'references' / 'README.md').write_text(references_readme)

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

const response = await fetch(url, {
const req = https.request(options, (res) => {
import axios from 'axios';
const http = axios.create({
console.log('🧪 Testing pure axios against Attio API...');
const probe = await http.get('/objects/companies');
import axios from 'axios';
const http = axios.create({
const probe = await http.get('/objects/companies');
import { AxiosInstance } from 'axios';
import { AxiosInstance } from 'axios';
import { AxiosInstance } from 'axios';
import { AxiosInstance } from 'axios';
import { AxiosInstance } from 'axios';
import { AxiosInstance } from 'axios';
import { AxiosInstance } from 'axios';
const client = axios.create({
const response = await axios.get(url, {
const optionsResponse = await axios.get(optionsUrl, {

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. (8 occurrence(s) shown as evidence).

grant_type: 'authorization_code',
grant_type: 'refresh_token',

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 Server();
const mcpServer = 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 →