agentsclimarketplace

Mcp server security scan

Skill Dolphinllc/claude-security-skills/skills/defensive/genai/mcp-server-security-scan

Defensive security skills for Claude Code and the Claude Agent SDK — web applications and generative AI systems.

Install
npx -y skills add Dolphinllc/claude-security-skills --skill mcp-server-security-scan

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Defensive security scan for Model Context Protocol (MCP) servers built with the official @modelcontextprotocol/sdk or mcp Python SDK. Detects tools exposing filesystem/shell with user-controlled paths, HTTP/SSE transports without authentication, unvalidated tool arguments, sensitive data leaking via resource URIs, and unbounded tool output that floods context. Invoke when the user asks to "review", "audit", or "scan" an MCP server implementation.

SKILL.md

6.4 KB, as published. Nobody here has run it

MCP Server Security Scan

Defensive scan for MCP servers (@modelcontextprotocol/sdk Node, mcp Python). Reports findings using the shared scoring schema.

Scope

  • Files importing @modelcontextprotocol/sdk/* or mcp (Python)
  • Tool / resource / prompt registrations (server.tool, @server.list_tools, etc.)
  • Transport setup (StdioServerTransport, SSEServerTransport, StreamableHTTPServerTransport)

Threat model recap

MCP tools execute on the server's trust boundary but are invoked at the LLM's discretion based on a user's conversation. Treat tool arguments as fully attacker-controlled: a user can craft chat input or upload a document that causes the model to call your tool with adversarial arguments (indirect prompt injection).

Rules

IDSeverityDetectionFix
MCP-FS-001criticalTool accepts path / filename argument and reads/writes the filesystem with no containment check (no path.resolve + startsWith(BASE))Resolve under a fixed base dir; reject if outside
MCP-FS-002highTool uses fs.readFile/fs.writeFile on a user-supplied path with no allowlist of base directoriesConfigure allowlist via env; refuse otherwise
MCP-EXEC-001criticalTool runs child_process.exec / subprocess.run(shell=True) with command built from argumentsUse spawn with arg array; allowlist commands; reject metacharacters
MCP-NET-001highTool performs HTTP fetch with arbitrary user URL (SSRF: localhost, RFC1918, link-local, AWS metadata 169.254.169.254)Resolve hostname; block private/link-local/loopback; allowlist if possible
MCP-AUTH-001criticalHTTP/SSE transport bound to non-loopback interface without authentication middleware (any process on the network can call tools)Require bearer token / mTLS; bind to loopback unless intentional
MCP-AUTH-002highStreamable HTTP transport without Origin/Host validation (DNS rebinding from a victim's browser to localhost MCP)Validate Origin; require non-browser auth header
MCP-VALID-001highTool registered without input schema (inputSchema / zod / Pydantic)Define and enforce schema; reject unknown fields
MCP-VALID-002mediumTool schema uses additionalProperties: true or z.any() / Dict[str, Any] for argumentsTighten schema
MCP-RES-001highResource URI scheme allows arbitrary paths (file:///{anything}) without containmentResolve under a base dir; refuse parent traversal
MCP-RES-002mediumResource list includes secrets (.env, key files, ~/.aws)Apply ignore list comparable to a .gitignore-style filter
MCP-OUT-001mediumTool returns unbounded output (full file, full table) that can flood the model's context window — DoS / cost amplificationCap output size; truncate with explicit notice
MCP-OUT-002highTool output forwards untrusted content (e.g., webpage fetched by tool) without marking provenanceTag output with source/trust metadata so the calling agent can apply data-vs-instruction framing
MCP-LOG-001mediumTool arguments / results logged to disk or remote sink without redactionLog tool name and run id; redact bodies
MCP-PROMPT-001mediumServer-defined prompts include literal credentials or per-tenant dataParameterize via prompt arguments; never bake secrets in
MCP-VER-001lowServer advertises capabilities it does not implement (e.g., declares tools but registers none)Match advertised capabilities to implementation

Wrong vs. right

MCP-FS-001 (path traversal)

// ❌ ../../etc/passwd
server.tool('read_doc', { path: z.string() }, async ({ path }) => {
  const text = await fs.readFile(path, 'utf8');
  return { content: [{ type: 'text', text }] };
});
// ✅ Containment + allowlist
const BASE = path.resolve(process.env.MCP_DOC_ROOT!);
server.tool(
  'read_doc',
  { path: z.string() },
  async ({ path: rel }) => {
    const target = path.resolve(BASE, rel);
    if (!target.startsWith(BASE + path.sep)) throw new Error('forbidden');
    const text = await fs.readFile(target, 'utf8');
    return { content: [{ type: 'text', text }] };
  },
);

MCP-AUTH-001 (HTTP transport without auth)

// ❌ Network-reachable, no auth
const transport = new StreamableHTTPServerTransport({ port: 8080 });
await server.connect(transport);
// ✅ Auth + Origin check + loopback default
const transport = new StreamableHTTPServerTransport({
  host: '127.0.0.1',
  port: 8080,
  requestHook: (req) => {
    if (req.headers.authorization !== `Bearer ${process.env.MCP_TOKEN}`) {
      throw new Error('unauthorized');
    }
    const origin = req.headers.origin;
    if (origin && !ALLOWED_ORIGINS.has(origin)) throw new Error('bad origin');
  },
});

MCP-NET-001 (SSRF)

# ❌ Tool fetches anything
@server.tool()
async def fetch(url: str) -> str:
    async with httpx.AsyncClient() as c:
        return (await c.get(url)).text
# ✅ DNS resolved + private-range blocked
import ipaddress, socket

BLOCKED = [ipaddress.ip_network(n) for n in
    ("127.0.0.0/8", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
     "169.254.0.0/16", "::1/128", "fc00::/7", "fe80::/10")]

def safe_host(host: str) -> bool:
    for _, _, _, _, sa in socket.getaddrinfo(host, None):
        ip = ipaddress.ip_address(sa[0])
        if any(ip in n for n in BLOCKED):
            return False
    return True

@server.tool()
async def fetch(url: str) -> str:
    parsed = urlparse(url)
    if parsed.scheme not in ("http", "https") or not safe_host(parsed.hostname):
        raise ValueError("forbidden")
    ...

References

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.