How MCP Least Privilege Detection Analyzes Code Capabilities in NVIDIA SkillSpector

The MCP least-privilege analyzer in NVIDIA SkillSpector compares declared permissions in SKILL.md manifests against actual code capabilities detected via regex patterns, generating LP1-LP4 findings to identify under-declared, over-declared, wildcard, or missing permissions.

The MCP least privilege detection system in NVIDIA SkillSpector provides automated static analysis to ensure Model Context Protocol (MCP) skills follow security best practices. This analyzer bridges the gap between what a skill claims to need in its manifest and what it actually executes in code. By scanning executable files for capability patterns and cross-referencing them against declared permissions, SkillSpector helps developers maintain the principle of least privilege.

How the MCP Least Privilege Analyzer Works

The analyzer processes skills through a static analysis pipeline defined in src/skillspector/nodes/analyzers/mcp_least_privilege.py.

Core Analysis Flow

The node(state: SkillspectorState) function orchestrates the validation process. It first loads the skill's manifest and file cache, then normalizes the permissions field to handle cases where permissions are absent or empty. If the skill contains only documentation files or lacks a manifest, the analyzer returns early with an empty finding list.

For each executable file in the component metadata, the analyzer extracts capabilities using _detect_capabilities, which applies regex patterns against the source text. Simultaneously, declared permissions are mapped to capability categories via _map_permissions_to_categories. The system then compares these two sets to generate specific findings.

Detecting Code Capabilities

The _CAPABILITY_PATTERNS dictionary contains case-insensitive regex patterns that identify six distinct capability categories: shell, network, file_read, file_write, env, and mcp.

For example, the network capability detection uses patterns matching common HTTP libraries:

"network": [
    r"\bhttpx\b",
    r"\brequests\b",
    r"\burllib\b",
    r"\baiohttp\b",
    r"socket\.connect",
    r"fetch\(",
    r"XMLHttpRequest",
],

These patterns are defined in mcp_least_privilege.py and applied to every executable file's raw text to build a map of detected capabilities per file path.

Mapping Permissions to Categories

The _PERM_TO_CAPABILITY dictionary translates common permission strings from the manifest into standardized capability categories. For instance, the permission "bash" maps to the "shell" category, while "network" maps directly to "network". This normalization enables consistent comparison between declared intents and actual code behavior.

The Four Least Privilege Findings (LP1-LP4)

The analyzer generates standardized findings based on NIST-style least privilege checks, each with specific severity and confidence levels.

LP1: Under-Declared Capabilities

LP1 identifies when code implements capabilities not covered by the manifest. If a capability is detected in the source code but its category is absent from the declared permissions (and no wildcard is present), the analyzer emits an LP1 finding with HIGH severity and 0.75 confidence. When the capability appears only in test files, confidence drops to 0.55.

LP2: Wildcard Permissions

LP2 triggers when the _has_wildcard function detects overly broad permissions. If any permission value matches entries in _WILDCARD_PERMS (*, all, full, or any), the analyzer reports a MEDIUM severity finding with confidence ≥0.90, as these grant blanket access beyond the principle of least privilege.

LP3: Missing Permissions with Active Capabilities

LP3 occurs when the manifest lacks a permissions field entirely (or it is empty) while the code contains executable capabilities. This generates a MEDIUM severity finding with confidence ≥0.70, flagging skills that operate without explicit permission declarations.

LP4: Over-Declared Permissions

LP4 detects the opposite problem: permissions declared in the manifest that have no corresponding implementation in the code. For each declared permission category with no detected capability, the analyzer emits a LOW severity finding with confidence ≥0.65, suggesting potential over-privileging.

Implementation Details in SkillSpector

The findings are constructed as instances of skillspector.models.Finding, defined in src/skillspector/models.py. Each finding includes rule_id, message, severity, confidence, category, and tags fields. Confidence values are clamped to the [0,1] range using the _clamp helper function.

Key constants in the analyzer include:

  • _WILDCARD_PERMS: {"*", "all", "full", "any"} (lines 38-39)
  • _CAPABILITY_PATTERNS: Regex mappings for the six capability categories (lines 40-60)
  • _PERM_TO_CAPABILITY: Permission string normalization mapping (lines 92-113)

Practical Code Examples

Running the Analyzer Locally

To execute the least privilege detection on a local skill directory:

import yaml
from pathlib import Path
from skillspector.nodes.analyzers import mcp_least_privilege

def build_state_from_dir(root: Path) -> dict:
    # Minimal subset of what `build_context` does – parses SKILL.md, builds caches.

    file_cache = {p.relative_to(root).as_posix(): p.read_text(encoding="utf-8")
                  for p in root.rglob("*") if p.is_file()}
    component_metadata = []
    exec_ext = {".py", ".sh", ".bash", ".zsh", ".js", ".ts", ".rb", ".go", ".rs", ".pl"}
    for p in root.rglob("*"):
        if not p.is_file():
            continue
        rel = p.relative_to(root).as_posix()
        suffix = p.suffix.lower()
        component_metadata.append({
            "path": rel,
            "type": suffix.lstrip("."),
            "executable": suffix in exec_ext,
            "lines": len(file_cache[rel].splitlines()),
            "size_bytes": p.stat().st_size,
        })
    # Parse manifest front‑matter (same logic as test helper)

    skill_md = file_cache.get("SKILL.md", "")
    manifest = {}
    if skill_md.startswith("---"):
        end = skill_md.find("\n---", 3)
        if end != -1:
            manifest = yaml.safe_load(skill_md[3:end])
    # Normalise permissions field to match analyzer expectations

    if isinstance(manifest.get("permissions"), list):
        manifest["permissions"] = [str(p) for p in manifest["permissions"]]
    else:
        manifest["permissions"] = None
    return {
        "manifest": manifest,
        "file_cache": file_cache,
        "component_metadata": component_metadata,
        "has_executable_scripts": any(m["executable"] for m in component_metadata),
        "components": list(file_cache.keys()),
    }

# Example usage

skill_dir = Path("/path/to/your/skill")
state = build_state_from_dir(skill_dir)
result = mcp_least_privilege.node(state)

for f in result["findings"]:
    print(f"{f.rule_id}: {f.message} (severity={f.severity}, confidence={f.confidence:.2f})")

Interpreting Output

Typical analyzer output reveals specific privilege violations:

LP2: Permission list contains a wildcard entry ('*', 'all', 'full', or 'any'), granting blanket access... (severity=MEDIUM, confidence=0.92)
LP1: Code capability 'network' detected in scripts/agent.py but not covered by declared permissions. (severity=HIGH, confidence=0.75)
LP4: Permission 'bash' is declared but no corresponding code capability (shell) was detected. (severity=LOW, confidence=0.65)

Unit Test State Construction

For testing purposes, you can construct a minimal state object:

state = {
    "manifest": {"name": "demo", "permissions": ["read"], "triggers": []},
    "file_cache": {
        "scripts/agent.py": "import httpx\nimport subprocess\nhttpx.get('http://example.com')\nsubprocess.run(['ls'])",
    },
    "component_metadata": [
        {"path": "scripts/agent.py", "type": "python", "executable": True,
         "lines": 4, "size_bytes": 120}
    ],
    "has_executable_scripts": True,
    "components": ["scripts/agent.py"],
}
findings = mcp_least_privilege.node(state)["findings"]
assert any(f.rule_id == "LP1" for f in findings)

Summary

  • The MCP least privilege analyzer in src/skillspector/nodes/analyzers/mcp_least_privilege.py performs static analysis comparing SKILL.md manifests against actual code capabilities.
  • It detects six capability categories (shell, network, file_read, file_write, env, mcp) using regex patterns defined in _CAPABILITY_PATTERNS.
  • Four finding types (LP1-LP4) identify under-declared capabilities, wildcard permissions, missing permissions, and over-declared permissions respectively.
  • Each finding includes severity levels (HIGH for LP1, MEDIUM for LP2/LP3, LOW for LP4) and confidence scores clamped between 0 and 1.
  • The analyzer skips documentation-only skills and normalizes permission strings via _PERM_TO_CAPABILITY for consistent comparison.

Frequently Asked Questions

What is MCP least privilege detection in SkillSpector?

MCP least privilege detection is a static analysis feature in NVIDIA SkillSpector that validates whether Model Context Protocol skills declare only the permissions they actually use. It compares the permissions list in SKILL.md against capabilities detected in executable code using regex patterns, generating findings when discrepancies exist.

How does SkillSpector detect capabilities in code?

SkillSpector uses the _detect_capabilities function in mcp_least_privilege.py to scan executable files for regex patterns that match imports and function calls associated with six categories: shell execution, network requests, file operations, environment access, and MCP interactions. These patterns are defined in the _CAPABILITY_PATTERNS dictionary.

What do the LP1-LP4 finding codes mean?

LP1 indicates under-declared capabilities (code uses permissions not listed in the manifest), LP2 flags wildcard permissions like * or all, LP3 identifies missing permission declarations when capabilities exist, and LP4 marks over-declared permissions (listed in manifest but unused in code). Each has distinct severity and confidence levels according to the NIST-style least privilege specification.

Which file types does the MCP least privilege analyzer examine?

The analyzer examines executable files based on extensions defined in the component metadata, including .py, .sh, .bash, .zsh, .js, .ts, .rb, .go, .rs, and .pl. It reads the raw text of these files and applies regex patterns to detect capabilities, while skipping non-executable documentation files.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →