DEV Community

AgentGraph
AgentGraph

Posted on

How to Audit Your MCP Servers for Security Risks

TL;DR: MCP servers run with surprising access to your filesystem, environment variables, and network — and most developers ship them without any security review. mcp-security-scan is an open-source CLI and GitHub Action that checks for credential theft, data exfiltration, and unsafe execution patterns, outputting a 0-100 trust score. It's free, MIT-licensed, and takes about 30 seconds to run.


If you've wired up an MCP server to Claude or another agent runtime, you've probably not thought too hard about what that server can actually do. That's normal. You were focused on getting the tool calls working.

But MCP servers run in a privileged position. They sit between your agent and the outside world, and they can read files, make network requests, spawn subprocesses, and access environment variables. The Model Context Protocol spec doesn't define a security model — it defines a communication protocol. What happens inside your server is entirely up to you.

That gap is where mcp-security-scan comes in.

What the scanner actually checks

The tool runs static analysis on your MCP server source code (TypeScript, Python, and JavaScript supported) and looks for six categories of risk:

Credential theft patterns — environment variable reads that touch anything matching *_KEY, *_SECRET, *_TOKEN, *_PASSWORD, or *_CREDENTIAL. Not all of these are bugs, but they should be visible. If your weather tool is reading STRIPE_SECRET_KEY, that's worth knowing.

Data exfiltration — outbound HTTP calls from inside tool handlers. Again, not inherently bad. But a tool that's supposed to format a string shouldn't be POSTing to an external endpoint.

Unsafe executionexec(), eval(), subprocess.run(), child_process.spawn(), and similar. These are the ones that keep security people up at night. An MCP tool that eval's user-supplied input is a remote code execution waiting to happen.

Filesystem access — reads and writes outside the working directory. Particularly dangerous when the agent is passing file paths that come from user input.

Code obfuscation — high-entropy strings, base64-encoded payloads, minified code shipped without source maps. These aren't automatically malicious, but they're a signal.

Dependency confusion risks — package names that shadow popular packages, or dependencies pulled from unusual registries.

Running it

npx @agentgraph/mcp-security-scan ./my-mcp-server
Enter fullscreen mode Exit fullscreen mode

Or install globally:

npm install -g @agentgraph/mcp-security-scan
mcp-security-scan ./my-mcp-server
Enter fullscreen mode Exit fullscreen mode

Python projects work the same way — the scanner detects the runtime from package.json or pyproject.toml.

Output looks like this:

AgentGraph MCP Security Scanner v0.4.1
Scanning: ./my-mcp-server

[PASS] No credential theft patterns detected
[WARN] 3 outbound HTTP calls in tool handlers (lines 47, 112, 203)
[FAIL] eval() usage detected in tools/execute.ts (line 89)
[WARN] Filesystem read outside working directory (tools/reader.ts, line 34)
[PASS] No obfuscated code detected
[PASS] Dependencies look clean

Trust Score: 61/100

Issues requiring attention:
  HIGH   eval() in tool handler — potential RCE vector
  MEDIUM Filesystem traversal — validate paths before use
  LOW    Outbound HTTP in 3 handlers — document or restrict

Run with --json for machine-readable output
Run with --fix for suggested remediations
Enter fullscreen mode Exit fullscreen mode

The trust score integrates directly with AgentGraph's trust badge system, so if you're publishing an MCP server for others to use, you can attach a verified score to your README.

The GitHub Action

Drop this in .github/workflows/security.yml:

name: MCP Security Scan

on:
  push:
    branches: [main]
  pull_request:

jobs:
  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run MCP Security Scan
        uses: agentgraph-co/mcp-security-scan@v1
        with:
          path: ./
          fail-below: 70
          output: sarif

      - name: Upload SARIF results
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: mcp-security-scan.sarif
Enter fullscreen mode Exit fullscreen mode

The fail-below threshold is configurable. We default to 70 for most projects — below that, you have at least one HIGH or several MEDIUMs that haven't been addressed. The SARIF output means findings show up natively in GitHub's Security tab, which is useful if you're already using CodeQL or similar.

Architecture decisions (and what we got wrong)

Building a security scanner for something as loosely-defined as MCP servers involved some choices worth being upfront about.

Static analysis vs. dynamic analysis. We chose static. Dynamic analysis would catch more — you'd actually run the server and observe its behavior. But it's dramatically harder to do safely. Running arbitrary MCP server code in a sandbox to observe what it does is the kind of thing that requires real infrastructure. Static analysis misses obfuscated runtime behavior, but it's fast, safe, and works in CI without any special setup.

The trade-off: a sufficiently motivated bad actor can evade static analysis. If someone really wants to hide malicious behavior, they'll encode it in a way that pattern-matching won't catch. We know this. The scanner isn't a guarantee — it's a baseline that catches the obvious stuff and raises the cost of shipping something obviously dangerous.

Regex vs. AST parsing. For the first version, we used regex patterns for Python and a lightweight AST walk for TypeScript/JavaScript. Regex is fast and easy to maintain, but it produces false positives. A comment that says # don't use eval() will still trigger the eval check. We're migrating the Python analyzer to use ast module parsing in v0.5, which will cut false positives significantly.

This is the thing about security tooling: false positives erode trust in the tool itself. If developers learn to ignore the warnings, the scanner becomes noise. We'd rather have fewer, higher-confidence findings than comprehensive but noisy output.

Trust score math. The 0-100 score is a weighted sum. HIGH findings cost 25 points each (capped at 2), MEDIUM findings cost 10 points each (capped at 3), LOW findings cost 3 points each. Starting from 100, you can floor at 0 but not go negative. This is... fine. It's not a sophisticated risk model. But it produces numbers that feel intuitively right for the cases we tested, and it's transparent enough that developers can understand why their score is what it is.

We considered a more sophisticated model — CVSS-style scoring with exploitability and impact dimensions. We decided against it for v1 because the complexity wasn't justified by the signal quality of static analysis. When you're doing AST pattern matching, you don't actually know if that eval() call is reachable from a tool handler or buried in dead code. Pretending you have CVSS-level precision would be misleading.

Why MCP security is messier than it looks

The Moltbook breach earlier this year exposed 1.5 million API tokens. That incident wasn't an MCP-specific attack, but the attack surface is similar: a platform that aggregates agent tools and credentials, with insufficient verification of what those tools actually do.

OpenClaw's skills marketplace had 12% of submissions flagged for malware in their last public audit. That's not a rounding error. That's one in eight tools doing something it shouldn't.

The problem is that MCP servers are easy to write and increasingly easy to publish. The friction between "I built a thing" and "other people's agents are running my thing" is very low. That's good for the ecosystem's growth. It's bad for security.

The Traceforce launch on HN this week (company-wide security monitoring for AI apps) is hitting the same problem from the enterprise end. They're watching what AI apps do at runtime. We're catching issues before deployment. Both approaches are necessary — the question of "is this agent doing what it claims to do" doesn't have a single answer at a single layer.

Using the API for custom integrations

If you want to integrate the scanner into something beyond a standard CI pipeline, there's a REST API:

import { MCPSecurityScanner } from '@agentgraph/mcp-security-scan';

const scanner = new MCPSecurityScanner({
  apiKey: process.env.AGENTGRAPH_API_KEY, // optional — enables trust badge publishing
  config: {
    failBelow: 70,
    checks: ['credentials', 'exfiltration', 'execution', 'filesystem'],
    exclude: ['node_modules', '**/*.test.ts']
  }
});

const result = await scanner.scan('./my-mcp-server');

console.log(`Trust Score: ${result.trustScore}`);
console.log(`Findings: ${result.findings.length}`);

for (const finding of result.findings) {
  console.log(`[${finding.severity}] ${finding.message} (${finding.file}:${finding.line})`);
}

// Publish to AgentGraph trust registry (requires API key)
if (result.trustScore >= 70) {
  const badge = await scanner.publishTrustBadge(result);
  console.log(`Badge URL: ${badge.url}`);
  console.log(`Embed in README: ${badge.markdown}`);
}
Enter fullscreen mode Exit fullscreen mode

The API key is only needed if you want to publish results to the AgentGraph trust registry and get a verified badge for your README. Scanning itself is entirely local — nothing leaves your machine without the API key configured.

What the scanner won't catch

Being honest about limitations:

Runtime behavior. If your server loads a malicious plugin at runtime from a URL that isn't in the source code, static analysis won't see it. This is a known gap.

Logic bugs. The scanner doesn't understand what your tool is supposed to do. If your tool is supposed to read files and it reads files, that's a WARN, not a FAIL — even if the specific files it reads are sensitive. You need a human to evaluate whether the behavior is appropriate.

Supply chain attacks in dependencies. We check package names for obvious confusion attacks, but we don't run a full audit of your dependency tree. Use npm audit or pip-audit for that — they're better tools for that specific job.

Obfuscated malice. A determined attacker who encodes their payload in a way that looks like a base64 config string will probably get through. We flag high-entropy strings, but the false positive rate on that check is high enough that many projects will tune it down.

The scanner is a floor, not a ceiling. It catches the stuff that shouldn't be there at all — the eval() calls, the credential reads in tools that don't need credentials, the outbound HTTP calls that weren't in the README.

Getting a trust badge for your MCP server

If you're publishing an MCP server — on npm, PyPI, or anywhere else — running the scanner and publishing your score takes about two minutes:

mcp-security-scan . --publish --api-key $AGENTGRAPH_API_KEY
Enter fullscreen mode Exit fullscreen mode

You get a badge like this in your README:

[![AgentGraph Trust Score](https://agentgraph.co/badge/your-server-id.svg)](https://agentgraph.co/server/your-server-id)
Enter fullscreen mode Exit fullscreen mode

The badge links to a public audit page showing what was scanned, what version, and what the findings were. It updates automatically when you push new code through the GitHub Action.

This is the piece that connects to the broader AgentGraph project. The scanner is useful standalone. But the trust badge is what makes the score meaningful to someone who's deciding whether to run your MCP server against their data.


The repo is at github.com/agentgraph-co/mcp-security-scan — MIT licensed, PRs open. We're particularly interested in contributions around the Python AST analyzer and additional check categories.

For the broader trust infrastructure context — DIDs, trust scoring, agent identity — that's at agentgraph.co. The scanner is one piece of it. The goal is a world where you can look at any agent or MCP server and have a real answer to "should I trust this thing."

Right now, the answer to that question is usually "I don't know, the README looked fine." That's not good enough.


This post was generated with AI assistance and reviewed by the AgentGraph team. We're committed to being transparent about that.

Top comments (0)