Skip to main content

Security scanner for Model Context Protocol (MCP) servers. Detects prompt injection, tool poisoning, capability escalation, and rug-pull attacks.

Project description

MCP Security Scanner

CI License Coverage PyPI

Security scanner for Model Context Protocol (MCP) servers. It analyzes server capabilities, detects policy and runtime risks, and exports findings as json, html, or sarif.

Why This Project

  • Secure MCP integrations before production rollout.
  • Detect static misconfiguration and capability risk early.
  • Compare baseline snapshots to catch risky tool mutations.
  • Optionally run bounded dynamic probes with --dynamic.

Architecture

flowchart LR
  A["Target Input (server/config/baseline/compare)"] --> B["Connector Discovery (stdio/sse/streamable-http)"]
  B --> C["Tool + Resource Snapshot"]
  C --> D["Static Analyzer Pipeline"]
  D --> E["Report Builder (json/html/sarif)"]
  C --> F["Baseline Engine (hash + mutation diff)"]
  A --> G["Optional Auth Resolver (config + URL JSON auth/mTLS)"]
  G --> B
  A --> H["Optional Dynamic Probes (--dynamic)"]
  H --> E
  F --> E

Capability Snapshot (Sprint 1-8AB)

Area Status
Transports stdio, sse, streamable-http
Commands server, config, baseline, compare, cache rotate
Default analyzers Static, PromptInjection, Escalation, ToolPoisoning, CrossTool
Dynamic mode Opt-in (--dynamic), bounded and deterministic
OAuth auth types oauth_client_credentials, oauth_device_code, oauth_auth_code_pkce
Token endpoint auth methods client_secret_post, client_secret_basic, private_key_jwt
Persistent cache backends local, aws_secrets_manager, aws_ssm_parameter_store, gcp_secret_manager, azure_key_vault, hashicorp_vault, kubernetes_secrets, oci_vault, doppler_secrets, onepassword_connect, bitwarden_secrets, infisical_secrets, akeyless_secrets, gitlab_variables, gitlab_group_variables, github_actions_variables, github_environment_variables, github_organization_variables, consul_kv, redis_kv, cloudflare_kv
Release pipeline OIDC publish + Sigstore + idempotent GitHub release + build-wheel CLI smoke + tag/version consistency guard (pyproject/__version__/wheel/CLI) + PyPI visibility verification
mTLS OAuth token-endpoint mTLS + transport discovery mTLS
Compare contract only tool_added, tool_removed, tool_changed mapped to LLM05

Current Scope Details

  • config supports auth/session flow v1 for network transports (bearer, api_key, session_cookie, oauth_client_credentials, oauth_device_code, oauth_auth_code_pkce)
  • Optional persistent OAuth cache hardening (strict lock, corruption recovery, metadata key management, multi-key recovery)
  • OAuth provider hardening+ (tolerant token parsing and transient retry policy for token endpoints)
  • OAuth provider integrations v2 in config auth: token_endpoint_auth_method=private_key_jwt supports env/file/AWS KMS signing sources
  • Release stabilization (Sprint 8D): PyPI distribution name switched to ogulcanaydogan-mcp-security-scanner to avoid name collision
  • Release hardening (Sprint 8J): publish workflow uses idempotent gh release create/upload path and tag-scoped publish concurrency guard
  • OAuth cache provider expansion (Sprint 8K): added aws_ssm_parameter_store backend (pre-provisioned SecureString parameter model)
  • OAuth cache provider expansion (Sprint 8L): added kubernetes_secrets backend (in-cluster auth + kubeconfig fallback, pre-provisioned Secret model)
  • OAuth cache provider expansion (Sprint 8M): added oci_vault backend (resource principal first, OCI config fallback, pre-provisioned secret model)
  • Release + contract hardening (Sprint 8N): pre-publish tag/version guard, post-publish PyPI visibility retry check, and shared OAuth cache backend invariant tests
  • OAuth cache provider expansion (Sprint 8O): added doppler_secrets backend (env-token auth, pre-provisioned secret model)
  • OAuth cache provider expansion (Sprint 8P): added onepassword_connect backend (env-token auth, pre-provisioned item/field model)
  • OAuth cache provider expansion (Sprint 8Q): added bitwarden_secrets backend (env-token auth, pre-provisioned secret model)
  • OAuth cache provider expansion (Sprint 8R): added infisical_secrets backend (env-token auth, pre-provisioned secret model)
  • OAuth cache provider expansion (Sprint 8S): added akeyless_secrets backend (env-token auth, pre-provisioned secret model)
  • OAuth cache provider expansion (Sprint 8T): added gitlab_variables backend (env-token auth, pre-provisioned project variable model)
  • OAuth cache provider expansion (Sprint 8U): added github_actions_variables backend (env-token auth, pre-provisioned repository variable model)
  • OAuth cache provider expansion (Sprint 8V): added github_environment_variables backend (env-token auth, pre-provisioned repository environment variable model)
  • OAuth cache provider expansion (Sprint 8W): added github_organization_variables backend (env-token auth, pre-provisioned organization variable model)
  • Release + contract hardening (Sprint 8X): added build-wheel CLI smoke verification, publish-time wheel/tag version guard, and expanded persistent=false backend invariant coverage
  • OAuth cache provider expansion (Sprint 8Y): added consul_kv backend (env-token auth, pre-provisioned KV key model)
  • OAuth cache provider expansion (Sprint 8Z): added redis_kv backend (env-password auth, pre-provisioned key model)
  • Stabilization hardening (Sprint 8AA): centralized OAuth cache backend dispatch contract + stricter publish-time version consistency checks (pyproject, __version__, wheel metadata, CLI)
  • OAuth cache provider expansion (Sprint 8AB): added cloudflare_kv backend (env-token auth, pre-provisioned KV key model)
  • OAuth cache provider expansion (Sprint 8AC): added gitlab_group_variables backend (env-token auth, pre-provisioned group variable model)
  • Baseline mutation detection (added / removed / changed) with deterministic hashes
  • Severity threshold filtering and documented exit-code contract

Installation

From PyPI (after trusted publisher mapping is enabled and first publish succeeds):

pip install ogulcanaydogan-mcp-security-scanner

From source:

git clone https://github.com/ogulcanaydogan/mcp-security-scanner.git
cd mcp-security-scanner
pip install -e .[dev]

PyPI Operations Checklist (Single Owner)

Current owner model is single-account (ogulcan). Keep these controls in place:

  • Ensure PyPI 2FA is enabled and recovery codes are stored offline.
  • Keep account email access current and verified before release windows.
  • In project publishing settings, keep exactly one active trusted publisher for:
    • repository ogulcanaydogan/mcp-security-scanner
    • workflow ci.yml
    • environment (Any) (empty)
  • Remove stale duplicate/pending publisher records for the same project.

Quick Start

# Version check
mcp-scan --version

# Scan a stdio server command
mcp-scan server "python -m my_mcp_server" --format json

# Scan a URL target (auto-detected: streamable-http, fallback to sse)
mcp-scan server "https://example.com/sse" --format html --output report.html

# Scan a URL target with auth/header/mTLS JSON options
mcp-scan server "https://example.com/mcp" \
  --headers-json '{"X-Trace":"run-42"}' \
  --auth-json '{"type":"api_key","key_env":"MCP_API_KEY"}' \
  --mtls-cert-file /etc/mcp/client.crt \
  --mtls-key-file /etc/mcp/client.key \
  --format json

# Run dynamic probes in addition to default analyzers (opt-in)
mcp-scan server "python -m my_mcp_server" --dynamic --format json

# Build baseline from live server snapshot
mcp-scan baseline "python -m my_mcp_server" --save baseline.json

# Compare live snapshot with baseline
mcp-scan compare baseline.json "python -m my_mcp_server" --format sarif --output mutations.sarif

# Rotate persistent OAuth cache encryption key
mcp-scan cache rotate

config Command (Claude Desktop Config)

mcp-scan config reads mcpServers entries and scans each server sequentially.

mcp-scan config ~/.claude/claude_desktop_config.json --timeout 30 --format json

Supported entry styles:

{
  "mcpServers": {
    "local-stdio": {
      "transport": "stdio",
      "command": "python",
      "args": ["-m", "my_mcp_server"],
      "env": {"APP_ENV": "prod"}
    },
    "remote-sse": {
      "transport": "sse",
      "url": "https://example.com/sse",
      "headers": {"X-Trace": "req-42"},
      "mtls_cert_file": "/etc/mcp/transport-client.crt",
      "mtls_key_file": "/etc/mcp/transport-client.key",
      "mtls_ca_bundle_file": "/etc/mcp/transport-ca.pem",
      "auth": {"type": "bearer", "token_env": "MCP_BEARER_TOKEN"}
    },
    "remote-streamable": {
      "transport": "streamable-http",
      "url": "https://example.com/mcp",
      "auth": {"type": "api_key", "key_env": "MCP_API_KEY", "header": "X-API-Key"}
    },
    "remote-session": {
      "transport": "sse",
      "url": "https://example.com/session",
      "headers": {"Cookie": "existing=1"},
      "auth": {"type": "session_cookie", "cookie_env": "MCP_SESSION_ID", "cookie_name": "session"}
    },
    "remote-oauth": {
      "transport": "streamable-http",
      "url": "https://example.com/mcp",
      "auth": {
        "type": "oauth_client_credentials",
        "token_url": "https://auth.example.com/oauth/token",
        "client_id_env": "MCP_OAUTH_CLIENT_ID",
        "token_endpoint_auth_method": "private_key_jwt",
        "client_assertion_kms_key_id": "arn:aws:kms:eu-west-1:111122223333:key/abcd",
        "client_assertion_kms_region": "eu-west-1",
        "client_assertion_kms_endpoint_url": "https://kms.eu-west-1.amazonaws.com",
        "client_assertion_kid": "key-2026-03",
        "mtls_cert_file": "/etc/mcp/oauth-client.crt",
        "mtls_key_file": "/etc/mcp/oauth-client.key",
        "mtls_ca_bundle_file": "/etc/mcp/oauth-ca.pem",
        "scope": "mcp.read",
        "audience": "mcp-security-scanner",
        "cache": {"persistent": true, "namespace": "prod-security", "backend": "local"},
        "header": "Authorization",
        "scheme": "Bearer"
      }
    },
    "remote-oauth-aws-cache": {
      "transport": "sse",
      "url": "https://example.com/sse",
      "auth": {
        "type": "oauth_client_credentials",
        "token_url": "https://auth.example.com/oauth/token",
        "client_id_env": "MCP_OAUTH_CLIENT_ID",
        "client_secret_env": "MCP_OAUTH_CLIENT_SECRET",
        "cache": {
          "persistent": true,
          "namespace": "prod-security",
          "backend": "aws_secrets_manager",
          "aws_secret_id": "mcp-security-scanner/oauth-cache-prod",
          "aws_region": "eu-west-1",
          "aws_endpoint_url": "https://secretsmanager.eu-west-1.amazonaws.com"
        }
      }
    },
    "remote-oauth-gcp-cache": {
      "transport": "streamable-http",
      "url": "https://example.com/mcp",
      "auth": {
        "type": "oauth_client_credentials",
        "token_url": "https://auth.example.com/oauth/token",
        "client_id_env": "MCP_OAUTH_CLIENT_ID",
        "client_secret_env": "MCP_OAUTH_CLIENT_SECRET",
        "cache": {
          "persistent": true,
          "namespace": "prod-security",
          "backend": "gcp_secret_manager",
          "gcp_secret_name": "projects/my-project/secrets/mcp-security-scanner-oauth-cache",
          "gcp_endpoint_url": "https://secretmanager.googleapis.com"
        }
      }
    },
    "remote-oauth-azure-cache": {
      "transport": "sse",
      "url": "https://example.com/sse",
      "auth": {
        "type": "oauth_client_credentials",
        "token_url": "https://auth.example.com/oauth/token",
        "client_id_env": "MCP_OAUTH_CLIENT_ID",
        "client_secret_env": "MCP_OAUTH_CLIENT_SECRET",
        "cache": {
          "persistent": true,
          "namespace": "prod-security",
          "backend": "azure_key_vault",
          "azure_vault_url": "https://mcp-security.vault.azure.net",
          "azure_secret_name": "mcp-security-scanner-oauth-cache",
          "azure_secret_version": "latest"
        }
      }
    },
    "remote-oauth-vault-cache": {
      "transport": "streamable-http",
      "url": "https://example.com/mcp",
      "auth": {
        "type": "oauth_client_credentials",
        "token_url": "https://auth.example.com/oauth/token",
        "client_id_env": "MCP_OAUTH_CLIENT_ID",
        "client_secret_env": "MCP_OAUTH_CLIENT_SECRET",
        "cache": {
          "persistent": true,
          "namespace": "prod-security",
          "backend": "hashicorp_vault",
          "vault_url": "https://vault.example.com",
          "vault_secret_path": "kv/mcp-security-scanner/oauth-cache",
          "vault_token_env": "VAULT_TOKEN",
          "vault_namespace": "platform-security"
        }
      }
    },
    "remote-oauth-k8s-cache": {
      "transport": "sse",
      "url": "https://example.com/sse",
      "auth": {
        "type": "oauth_client_credentials",
        "token_url": "https://auth.example.com/oauth/token",
        "client_id_env": "MCP_OAUTH_CLIENT_ID",
        "client_secret_env": "MCP_OAUTH_CLIENT_SECRET",
        "cache": {
          "persistent": true,
          "namespace": "prod-security",
          "backend": "kubernetes_secrets",
          "k8s_secret_namespace": "mcp-security",
          "k8s_secret_name": "oauth-cache",
          "k8s_secret_key": "oauth_cache"
        }
      }
    },
    "remote-oauth-oci-cache": {
      "transport": "streamable-http",
      "url": "https://example.com/mcp",
      "auth": {
        "type": "oauth_client_credentials",
        "token_url": "https://auth.example.com/oauth/token",
        "client_id_env": "MCP_OAUTH_CLIENT_ID",
        "client_secret_env": "MCP_OAUTH_CLIENT_SECRET",
        "cache": {
          "persistent": true,
          "namespace": "prod-security",
          "backend": "oci_vault",
          "oci_secret_ocid": "ocid1.secret.oc1.iad.exampleuniqueid1234567890",
          "oci_region": "eu-frankfurt-1",
          "oci_endpoint_url": "https://vaults.eu-frankfurt-1.oci.oraclecloud.com"
        }
      }
    },
    "remote-oauth-doppler-cache": {
      "transport": "sse",
      "url": "https://example.com/sse",
      "auth": {
        "type": "oauth_client_credentials",
        "token_url": "https://auth.example.com/oauth/token",
        "client_id_env": "MCP_OAUTH_CLIENT_ID",
        "client_secret_env": "MCP_OAUTH_CLIENT_SECRET",
        "cache": {
          "persistent": true,
          "namespace": "prod-security",
          "backend": "doppler_secrets",
          "doppler_project": "security-platform",
          "doppler_config": "prd",
          "doppler_secret_name": "MCP_OAUTH_CACHE",
          "doppler_token_env": "DOPPLER_TOKEN",
          "doppler_api_url": "https://api.doppler.com"
        }
      }
    },
    "remote-oauth-onepassword-cache": {
      "transport": "streamable-http",
      "url": "https://example.com/mcp",
      "auth": {
        "type": "oauth_client_credentials",
        "token_url": "https://auth.example.com/oauth/token",
        "client_id_env": "MCP_OAUTH_CLIENT_ID",
        "client_secret_env": "MCP_OAUTH_CLIENT_SECRET",
        "cache": {
          "persistent": true,
          "namespace": "prod-security",
          "backend": "onepassword_connect",
          "op_connect_host": "https://op-connect.example.com",
          "op_vault_id": "vault-uuid-or-id",
          "op_item_id": "item-uuid",
          "op_field_label": "oauth_cache",
          "op_connect_token_env": "OP_CONNECT_TOKEN"
        }
      }
    },
    "remote-oauth-bitwarden-cache": {
      "transport": "sse",
      "url": "https://example.com/sse",
      "auth": {
        "type": "oauth_client_credentials",
        "token_url": "https://auth.example.com/oauth/token",
        "client_id_env": "MCP_OAUTH_CLIENT_ID",
        "client_secret_env": "MCP_OAUTH_CLIENT_SECRET",
        "cache": {
          "persistent": true,
          "namespace": "prod-security",
          "backend": "bitwarden_secrets",
          "bw_secret_id": "11111111-2222-3333-4444-555555555555",
          "bw_access_token_env": "BWS_ACCESS_TOKEN",
          "bw_api_url": "https://api.bitwarden.com"
        }
      }
    },
    "remote-oauth-infisical-cache": {
      "transport": "streamable-http",
      "url": "https://example.com/mcp",
      "auth": {
        "type": "oauth_client_credentials",
        "token_url": "https://auth.example.com/oauth/token",
        "client_id_env": "MCP_OAUTH_CLIENT_ID",
        "client_secret_env": "MCP_OAUTH_CLIENT_SECRET",
        "cache": {
          "persistent": true,
          "namespace": "prod-security",
          "backend": "infisical_secrets",
          "infisical_project_id": "workspace-123",
          "infisical_environment": "prod",
          "infisical_secret_name": "MCP_OAUTH_CACHE",
          "infisical_token_env": "INFISICAL_TOKEN",
          "infisical_api_url": "https://app.infisical.com/api"
        }
      }
    },
    "remote-oauth-akeyless-cache": {
      "transport": "streamable-http",
      "url": "https://example.com/mcp",
      "auth": {
        "type": "oauth_client_credentials",
        "token_url": "https://auth.example.com/oauth/token",
        "client_id_env": "MCP_OAUTH_CLIENT_ID",
        "client_secret_env": "MCP_OAUTH_CLIENT_SECRET",
        "cache": {
          "persistent": true,
          "namespace": "prod-security",
          "backend": "akeyless_secrets",
          "akeyless_secret_name": "/prod/mcp/oauth_cache",
          "akeyless_token_env": "AKEYLESS_TOKEN",
          "akeyless_api_url": "https://api.akeyless.io"
        }
      }
    },
    "remote-oauth-gitlab-cache": {
      "transport": "sse",
      "url": "https://example.com/sse",
      "auth": {
        "type": "oauth_client_credentials",
        "token_url": "https://auth.example.com/oauth/token",
        "client_id_env": "MCP_OAUTH_CLIENT_ID",
        "client_secret_env": "MCP_OAUTH_CLIENT_SECRET",
        "cache": {
          "persistent": true,
          "namespace": "prod-security",
          "backend": "gitlab_variables",
          "gitlab_project_id": "12345",
          "gitlab_variable_key": "MCP_OAUTH_CACHE",
          "gitlab_token_env": "GITLAB_TOKEN",
          "gitlab_api_url": "https://gitlab.example.com/api/v4"
        }
      }
    },
    "remote-oauth-gitlab-group-cache": {
      "transport": "sse",
      "url": "https://example.com/sse",
      "auth": {
        "type": "oauth_client_credentials",
        "token_url": "https://auth.example.com/oauth/token",
        "client_id_env": "MCP_OAUTH_CLIENT_ID",
        "client_secret_env": "MCP_OAUTH_CLIENT_SECRET",
        "cache": {
          "persistent": true,
          "namespace": "prod-security",
          "backend": "gitlab_group_variables",
          "gitlab_group_id": "67890",
          "gitlab_variable_key": "MCP_OAUTH_CACHE",
          "gitlab_token_env": "GITLAB_TOKEN",
          "gitlab_api_url": "https://gitlab.example.com/api/v4"
        }
      }
    },
    "remote-oauth-github-cache": {
      "transport": "streamable-http",
      "url": "https://example.com/mcp",
      "auth": {
        "type": "oauth_client_credentials",
        "token_url": "https://auth.example.com/oauth/token",
        "client_id_env": "MCP_OAUTH_CLIENT_ID",
        "client_secret_env": "MCP_OAUTH_CLIENT_SECRET",
        "cache": {
          "persistent": true,
          "namespace": "prod-security",
          "backend": "github_actions_variables",
          "github_repository": "ogulcanaydogan/mcp-security-scanner",
          "github_variable_name": "MCP_OAUTH_CACHE",
          "github_token_env": "GITHUB_TOKEN",
          "github_api_url": "https://api.github.com"
        }
      }
    },
    "remote-oauth-github-environment-cache": {
      "transport": "streamable-http",
      "url": "https://example.com/mcp",
      "auth": {
        "type": "oauth_client_credentials",
        "token_url": "https://auth.example.com/oauth/token",
        "client_id_env": "MCP_OAUTH_CLIENT_ID",
        "client_secret_env": "MCP_OAUTH_CLIENT_SECRET",
        "cache": {
          "persistent": true,
          "namespace": "prod-security",
          "backend": "github_environment_variables",
          "github_repository": "ogulcanaydogan/mcp-security-scanner",
          "github_environment_name": "production",
          "github_variable_name": "MCP_OAUTH_CACHE",
          "github_token_env": "GITHUB_TOKEN",
          "github_api_url": "https://api.github.com"
        }
      }
    },
    "remote-oauth-github-organization-cache": {
      "transport": "streamable-http",
      "url": "https://example.com/mcp",
      "auth": {
        "type": "oauth_client_credentials",
        "token_url": "https://auth.example.com/oauth/token",
        "client_id_env": "MCP_OAUTH_CLIENT_ID",
        "client_secret_env": "MCP_OAUTH_CLIENT_SECRET",
        "cache": {
          "persistent": true,
          "namespace": "prod-security",
          "backend": "github_organization_variables",
          "github_organization": "ogulcanaydogan",
          "github_variable_name": "MCP_OAUTH_CACHE",
          "github_token_env": "GITHUB_TOKEN",
          "github_api_url": "https://api.github.com"
        }
      }
    },
    "remote-oauth-consul-cache": {
      "transport": "streamable-http",
      "url": "https://example.com/mcp",
      "auth": {
        "type": "oauth_client_credentials",
        "token_url": "https://auth.example.com/oauth/token",
        "client_id_env": "MCP_OAUTH_CLIENT_ID",
        "client_secret_env": "MCP_OAUTH_CLIENT_SECRET",
        "cache": {
          "persistent": true,
          "namespace": "prod-security",
          "backend": "consul_kv",
          "consul_key_path": "mcp/security/oauth/cache",
          "consul_token_env": "CONSUL_HTTP_TOKEN",
          "consul_api_url": "https://consul.example.com"
        }
      }
    },
    "remote-oauth-redis-cache": {
      "transport": "streamable-http",
      "url": "https://example.com/mcp",
      "auth": {
        "type": "oauth_client_credentials",
        "token_url": "https://auth.example.com/oauth/token",
        "client_id_env": "MCP_OAUTH_CLIENT_ID",
        "client_secret_env": "MCP_OAUTH_CLIENT_SECRET",
        "cache": {
          "persistent": true,
          "namespace": "prod-security",
          "backend": "redis_kv",
          "redis_key": "mcp/security/oauth/cache",
          "redis_url": "rediss://redis.example.com:6380/0",
          "redis_password_env": "REDIS_PASSWORD"
        }
      }
    },
    "remote-oauth-cloudflare-cache": {
      "transport": "streamable-http",
      "url": "https://example.com/mcp",
      "auth": {
        "type": "oauth_client_credentials",
        "token_url": "https://auth.example.com/oauth/token",
        "client_id_env": "MCP_OAUTH_CLIENT_ID",
        "client_secret_env": "MCP_OAUTH_CLIENT_SECRET",
        "cache": {
          "persistent": true,
          "namespace": "prod-security",
          "backend": "cloudflare_kv",
          "cf_account_id": "1234567890abcdef1234567890abcdef",
          "cf_namespace_id": "fedcba0987654321fedcba0987654321",
          "cf_kv_key": "mcp/security/oauth/cache",
          "cf_api_token_env": "CLOUDFLARE_API_TOKEN",
          "cf_api_url": "https://api.cloudflare.com/client/v4"
        }
      }
    },
    "remote-device-oauth": {
      "transport": "sse",
      "url": "https://example.com/sse",
      "auth": {
        "type": "oauth_device_code",
        "device_authorization_url": "https://auth.example.com/oauth/device/code",
        "token_url": "https://auth.example.com/oauth/token",
        "client_id_env": "MCP_OAUTH_DEVICE_CLIENT_ID",
        "client_secret_env": "MCP_OAUTH_DEVICE_CLIENT_SECRET",
        "token_endpoint_auth_method": "client_secret_post",
        "scope": "mcp.read",
        "audience": "mcp-security-scanner",
        "header": "Authorization",
        "scheme": "Bearer"
      }
    },
    "remote-auth-code": {
      "transport": "streamable-http",
      "url": "https://example.com/mcp",
      "auth": {
        "type": "oauth_auth_code_pkce",
        "authorization_url": "https://auth.example.com/oauth/authorize",
        "token_url": "https://auth.example.com/oauth/token",
        "client_id_env": "MCP_OAUTH_AUTH_CODE_CLIENT_ID",
        "scope": "mcp.read",
        "audience": "mcp-security-scanner",
        "redirect_host": "127.0.0.1",
        "redirect_port": 8765,
        "callback_path": "/callback"
      }
    }
  }
}

Notes:

  • stdio validation: command required, args optional list, env optional object
  • sse validation: url required (http/https), headers optional object
  • streamable-http validation: url required (http/https), headers optional object
  • transport: "streamable_http" alias is accepted and normalized to streamable-http
  • auth is optional and only valid for sse/streamable-http entries
  • auth validation/env resolution errors produce auth_config_error findings and scan continues with remaining servers
  • OAuth token endpoint/network/response failures produce auth_token_error findings and scan continues with remaining servers
  • oauth_client_credentials and oauth_device_code support optional token_endpoint_auth_method:
    • client_secret_post (default)
    • client_secret_basic (oauth_device_code requires client_secret_env when used)
    • private_key_jwt (oauth_client_credentials + oauth_device_code; oauth_auth_code_pkce remains unchanged)
  • private_key_jwt validation rules:
    • exactly one signing source is required:
      • client_assertion_key_env
      • client_assertion_key_file
      • client_assertion_kms_key_id (AWS KMS signing)
    • optional KMS tuning: client_assertion_kms_region, client_assertion_kms_endpoint_url
    • optional client_assertion_kid is propagated into JWT header
    • v1 signing algorithm is RS256
  • token endpoint mTLS options for OAuth auth entries:
    • mtls_cert_file + mtls_key_file must be provided together
    • optional mtls_ca_bundle_file is used as request verify bundle
    • mTLS is applied only to OAuth token endpoint calls
  • transport-level mTLS options for network entries (sse, streamable-http):
    • top-level mtls_cert_file + mtls_key_file must be provided together
    • optional top-level mtls_ca_bundle_file is used as connection verify bundle
    • applies to discovery transport HTTP client setup (independent from auth.mtls_*)
  • OAuth token cache key is deterministic: namespace + token_url + client_id + scope + audience
  • auth.cache is optional and only valid for OAuth auth types:
    • persistent (bool, default false)
    • namespace (string, default "default")
    • backend (string, default "local"): local, aws_secrets_manager, aws_ssm_parameter_store, gcp_secret_manager, azure_key_vault, hashicorp_vault, kubernetes_secrets, oci_vault, doppler_secrets, onepassword_connect, bitwarden_secrets, infisical_secrets, akeyless_secrets, gitlab_variables, gitlab_group_variables, github_actions_variables, github_environment_variables, github_organization_variables, consul_kv, redis_kv, or cloudflare_kv
    • aws_secret_id (required when backend=aws_secrets_manager)
    • aws_ssm_parameter_name (required when backend=aws_ssm_parameter_store)
    • optional aws_region, aws_endpoint_url for AWS client routing (aws_secrets_manager / aws_ssm_parameter_store)
    • gcp_secret_name (required when backend=gcp_secret_manager, format projects/<project>/secrets/<secret>)
    • optional gcp_endpoint_url for GCP client endpoint routing (ADC auth)
    • azure_vault_url (required when backend=azure_key_vault, format https://<name>.vault.azure.net)
    • azure_secret_name (required when backend=azure_key_vault, Azure Key Vault secret-name rules)
    • optional azure_secret_version (default latest)
    • vault_url (required when backend=hashicorp_vault, http/https)
    • vault_secret_path (required when backend=hashicorp_vault, KV path)
    • optional vault_token_env (Vault token env var name; defaults to VAULT_TOKEN)
    • optional vault_namespace
    • k8s_secret_namespace (required when backend=kubernetes_secrets, Kubernetes namespace)
    • k8s_secret_name (required when backend=kubernetes_secrets, Kubernetes Secret name)
    • optional k8s_secret_key (default oauth_cache)
    • oci_secret_ocid (required when backend=oci_vault, OCI secret OCID)
    • optional oci_region
    • optional oci_endpoint_url (http/https)
    • doppler_project (required when backend=doppler_secrets)
    • doppler_config (required when backend=doppler_secrets)
    • doppler_secret_name (required when backend=doppler_secrets)
    • optional doppler_token_env (default DOPPLER_TOKEN)
    • optional doppler_api_url (https URL; defaults to Doppler API)
    • op_connect_host (required when backend=onepassword_connect, https URL)
    • op_vault_id (required when backend=onepassword_connect)
    • op_item_id (required when backend=onepassword_connect)
    • optional op_field_label (default oauth_cache)
    • optional op_connect_token_env (default OP_CONNECT_TOKEN)
    • bw_secret_id (required when backend=bitwarden_secrets, UUID-style secret ID)
    • optional bw_access_token_env (default BWS_ACCESS_TOKEN)
    • optional bw_api_url (https URL; defaults to Bitwarden API)
    • infisical_project_id (required when backend=infisical_secrets)
    • infisical_environment (required when backend=infisical_secrets)
    • infisical_secret_name (required when backend=infisical_secrets)
    • optional infisical_token_env (default INFISICAL_TOKEN)
    • optional infisical_api_url (https URL; defaults to Infisical cloud API)
    • akeyless_secret_name (required when backend=akeyless_secrets)
    • optional akeyless_token_env (default AKEYLESS_TOKEN)
    • optional akeyless_api_url (https URL; defaults to Akeyless API)
    • gitlab_project_id (required when backend=gitlab_variables, numeric project ID)
    • gitlab_group_id (required when backend=gitlab_group_variables, numeric group ID)
    • gitlab_variable_key (required when backend=gitlab_variables or backend=gitlab_group_variables, env-style key)
    • optional gitlab_token_env (default GITLAB_TOKEN)
    • optional gitlab_api_url (https URL; defaults to https://gitlab.com/api/v4)
    • github_repository (required when backend=github_actions_variables or backend=github_environment_variables, format <owner>/<repo>)
    • github_organization (required when backend=github_organization_variables, organization slug)
    • github_environment_name (required when backend=github_environment_variables, non-empty string)
    • github_variable_name (required when backend=github_actions_variables, backend=github_environment_variables, or backend=github_organization_variables, env-style key)
    • optional github_token_env (default GITHUB_TOKEN)
    • optional github_api_url (https URL; defaults to https://api.github.com)
    • consul_key_path (required when backend=consul_kv, Consul KV key path)
    • optional consul_token_env (default CONSUL_HTTP_TOKEN)
    • optional consul_api_url (http/https URL; defaults to http://127.0.0.1:8500)
    • redis_key (required when backend=redis_kv, Redis key path)
    • optional redis_url (redis:// or rediss://; defaults to redis://127.0.0.1:6379/0)
    • optional redis_password_env (default REDIS_PASSWORD)
    • cf_account_id (required when backend=cloudflare_kv, Cloudflare account identifier)
    • cf_namespace_id (required when backend=cloudflare_kv, Cloudflare KV namespace identifier)
    • cf_kv_key (required when backend=cloudflare_kv, Cloudflare KV key)
    • optional cf_api_token_env (default CLOUDFLARE_API_TOKEN)
    • optional cf_api_url (https URL; defaults to https://api.cloudflare.com/client/v4)
  • cache lookup order for OAuth:
    • in-memory
    • persistent disk cache (auth.cache.persistent=true)
    • refresh grant
    • primary grant
  • persistent cache details (opt-in):
    • backend=local:
      • encrypted file: ~/.cache/mcp-security-scanner/oauth-cache-v1.json.enc
      • lock file: ~/.cache/mcp-security-scanner/oauth-cache-v1.lock (exclusive lock with retry; timeout falls back to in-memory/live token flow)
      • encrypted payload envelope: schema_version, key_id, updated_at, entries (v2)
      • encryption key lookup: OS keyring (service="mcp-security-scanner", username="oauth-cache-key-v1") then fallback key file ~/.config/mcp-security-scanner/cache.key
      • key metadata stores active + historical key entries (key_id + fernet_key); legacy raw key format remains readable
      • decrypt recovery order: payload key_id match when possible, then active key, then historical keys (deterministic order)
      • historical key retention is bounded (max 3); cache rotate promotes current active key into historical set
      • fallback key file is created with 0600 permissions
      • cache/key file mode hardening uses best-effort 0600
      • corrupt or undecryptable cache payloads are quarantined as oauth-cache-v1.json.enc.corrupt.<timestamp>
    • backend=aws_secrets_manager:
      • cache payload is stored as a single JSON envelope in the configured AWS secret (auth.cache.aws_secret_id)
      • optional aws_region and aws_endpoint_url tune client resolution
    • backend=aws_ssm_parameter_store:
      • cache payload is stored as a single JSON envelope in configured SSM SecureString parameter (auth.cache.aws_ssm_parameter_name)
      • parameter must be pre-provisioned; missing/provider errors are non-fatal and scanner falls back to live token flow
      • optional aws_region and aws_endpoint_url tune client resolution
    • backend=gcp_secret_manager:
      • cache payload is stored as a single JSON envelope in the configured GCP secret (auth.cache.gcp_secret_name)
      • writes use new secret versions via projects/.../secrets/.../versions/latest read + add_secret_version write
      • secret must be pre-provisioned; missing/provider errors are non-fatal and scanner falls back to live token flow
      • optional gcp_endpoint_url is supported for custom endpoint routing; auth uses ADC
    • backend=azure_key_vault:
      • cache payload is stored as a single JSON envelope in the configured Azure Key Vault secret (auth.cache.azure_secret_name)
      • auth uses Azure SDK default credential chain (DefaultAzureCredential), no new CLI credential flags
      • secret must be pre-provisioned; missing/provider errors are non-fatal and scanner falls back to live token flow
      • optional azure_secret_version controls read version (default latest); writes create a new secret version
    • backend=hashicorp_vault:
      • cache payload is stored as a single JSON envelope in configured Vault KV v2 secret path (auth.cache.vault_secret_path)
      • auth uses configured token env (vault_token_env) or VAULT_TOKEN fallback
      • optional Vault enterprise namespace is supported via vault_namespace
      • secret path must be pre-provisioned; missing/provider errors are non-fatal and scanner falls back to live token flow
    • backend=kubernetes_secrets:
      • cache payload is stored as a single JSON envelope in configured Kubernetes Secret data key (auth.cache.k8s_secret_namespace / auth.cache.k8s_secret_name / auth.cache.k8s_secret_key)
      • Kubernetes auth chain is deterministic: in-cluster config first, then kubeconfig fallback
      • Secret must be pre-provisioned; scanner patches existing Secret data and does not auto-create missing Secrets
      • missing/provider/read/write/parse errors are non-fatal and scanner falls back to live token flow
    • backend=oci_vault:
      • cache payload is stored as a single JSON envelope in configured OCI Vault secret (auth.cache.oci_secret_ocid)
      • auth chain is deterministic: Resource Principal signer first, then OCI config/profile fallback
      • secret must be pre-provisioned; scanner writes new secret content version and does not auto-create missing secrets
      • missing/provider/read/write/parse errors are non-fatal and scanner falls back to live token flow
    • backend=doppler_secrets:
      • cache payload is stored as a single JSON envelope in configured Doppler secret (auth.cache.doppler_project / auth.cache.doppler_config / auth.cache.doppler_secret_name)
      • auth uses env token only (auth.cache.doppler_token_env, default DOPPLER_TOKEN)
      • secret must be pre-provisioned; scanner updates existing secret key and does not auto-create missing secrets
      • missing/provider/read/write/parse errors are non-fatal and scanner falls back to live token flow
    • backend=onepassword_connect:
      • cache payload is stored as a single JSON envelope in configured 1Password Connect item field (auth.cache.op_connect_host / auth.cache.op_vault_id / auth.cache.op_item_id / auth.cache.op_field_label)
      • auth uses env token only (auth.cache.op_connect_token_env, default OP_CONNECT_TOKEN)
      • item+field must be pre-provisioned; scanner updates existing field value and does not auto-create missing item/field
      • missing/provider/read/write/parse errors are non-fatal and scanner falls back to live token flow
    • backend=bitwarden_secrets:
      • cache payload is stored as a single JSON envelope in configured Bitwarden secret value (auth.cache.bw_secret_id)
      • auth uses env token only (auth.cache.bw_access_token_env, default BWS_ACCESS_TOKEN)
      • secret must be pre-provisioned; scanner updates existing secret value and does not auto-create missing secrets
      • missing/provider/read/write/parse errors are non-fatal and scanner falls back to live token flow
    • backend=infisical_secrets:
      • cache payload is stored as a single JSON envelope in configured Infisical secret value (auth.cache.infisical_project_id / auth.cache.infisical_environment / auth.cache.infisical_secret_name)
      • auth uses env token only (auth.cache.infisical_token_env, default INFISICAL_TOKEN)
      • secret must be pre-provisioned; scanner updates existing secret value and does not auto-create missing secrets
      • missing/provider/read/write/parse errors are non-fatal and scanner falls back to live token flow
    • backend=akeyless_secrets:
      • cache payload is stored as a single JSON envelope in configured Akeyless secret value (auth.cache.akeyless_secret_name)
      • auth uses env token only (auth.cache.akeyless_token_env, default AKEYLESS_TOKEN)
      • secret must be pre-provisioned; scanner updates existing secret value and does not auto-create missing secrets
      • missing/provider/read/write/parse errors are non-fatal and scanner falls back to live token flow
    • backend=gitlab_variables:
      • cache payload is stored as a single JSON envelope in configured GitLab project variable value (auth.cache.gitlab_project_id / auth.cache.gitlab_variable_key)
      • auth uses env token only (auth.cache.gitlab_token_env, default GITLAB_TOKEN)
      • variable must be pre-provisioned; scanner updates existing variable value and does not auto-create missing variables
      • missing/provider/read/write/parse errors are non-fatal and scanner falls back to live token flow
    • backend=gitlab_group_variables:
      • cache payload is stored as a single JSON envelope in configured GitLab group variable value (auth.cache.gitlab_group_id / auth.cache.gitlab_variable_key)
      • auth uses env token only (auth.cache.gitlab_token_env, default GITLAB_TOKEN)
      • variable must be pre-provisioned; scanner updates existing variable value and does not auto-create missing variables
      • missing/provider/read/write/parse errors are non-fatal and scanner falls back to live token flow
    • backend=github_actions_variables:
      • cache payload is stored as a single JSON envelope in configured GitHub repository Actions variable value (auth.cache.github_repository / auth.cache.github_variable_name)
      • auth uses env token only (auth.cache.github_token_env, default GITHUB_TOKEN)
      • variable must be pre-provisioned; scanner updates existing variable value and does not auto-create missing variables
      • missing/provider/read/write/parse errors are non-fatal and scanner falls back to live token flow
    • backend=github_environment_variables:
      • cache payload is stored as a single JSON envelope in configured GitHub repository environment variable value (auth.cache.github_repository / auth.cache.github_environment_name / auth.cache.github_variable_name)
      • auth uses env token only (auth.cache.github_token_env, default GITHUB_TOKEN)
      • environment + variable must be pre-provisioned; scanner updates existing variable value and does not auto-create missing variables
      • environment/variable path segments are URL-encoded before API calls
      • missing/provider/read/write/parse errors are non-fatal and scanner falls back to live token flow
    • backend=github_organization_variables:
      • cache payload is stored as a single JSON envelope in configured GitHub organization variable value (auth.cache.github_organization / auth.cache.github_variable_name)
      • auth uses env token only (auth.cache.github_token_env, default GITHUB_TOKEN)
      • organization variable must be pre-provisioned; scanner updates existing variable value and does not auto-create missing variables
      • organization/variable path segments are URL-encoded before API calls
      • missing/provider/read/write/parse errors are non-fatal and scanner falls back to live token flow
    • backend=consul_kv:
      • cache payload is stored as a single JSON envelope in configured Consul KV key (auth.cache.consul_key_path)
      • auth uses env token only (auth.cache.consul_token_env, default CONSUL_HTTP_TOKEN)
      • key must be pre-provisioned; scanner updates existing key value and does not auto-create missing keys
      • key path segments are URL-encoded (with / preserved) before API calls
      • missing/provider/read/write/parse errors are non-fatal and scanner falls back to live token flow
    • backend=redis_kv:
      • cache payload is stored as a single JSON envelope in configured Redis key (auth.cache.redis_key)
      • auth uses env password only (auth.cache.redis_password_env, default REDIS_PASSWORD)
      • key must be pre-provisioned; scanner updates existing key value and does not auto-create missing keys
      • supports redis:// and rediss:// URL forms via auth.cache.redis_url
      • missing/provider/read/write/parse errors are non-fatal and scanner falls back to live token flow
    • backend=cloudflare_kv:
      • cache payload is stored as a single JSON envelope in configured Cloudflare KV key (auth.cache.cf_account_id / auth.cache.cf_namespace_id / auth.cache.cf_kv_key)
      • auth uses env token only (auth.cache.cf_api_token_env, default CLOUDFLARE_API_TOKEN)
      • key must be pre-provisioned; scanner updates existing key value and does not auto-create missing keys
      • account/namespace/key path segments are URL-encoded before API calls
      • missing/provider/read/write/parse errors are non-fatal and scanner falls back to live token flow
    • backend read/write/decrypt/parse failures are non-fatal; scanner falls back to live token flow
  • oauth_device_code uses copy/paste UX (verification_uri + user_code) and supports refresh-token reuse on expiry
  • in headless/CI environments (no interactive TTY), oauth_device_code entries produce auth_token_error and scan continues
  • oauth_auth_code_pkce uses local callback + PKCE (S256), prints authorization URL, and supports refresh-token reuse on expiry
  • oauth_auth_code_pkce callback listener tries configured/default port first and falls back to random local port when needed
  • in headless/CI environments (no interactive TTY), oauth_auth_code_pkce entries produce auth_token_error and scan continues
  • Authorization header scheme precedence for OAuth is:
    • auth.scheme (if provided)
    • token response token_type (if present)
    • fallback Bearer
  • OAuth token/device/refresh/auth-code endpoint calls use shared transient retry policy:
    • retryable statuses: 429, 500, 502, 503, 504
    • retryable transport errors: timeout/connection/network
    • max 2 retries (total 3 attempts), short bounded backoff
  • dynamic analyzer v1 is opt-in:
    • enable with --dynamic on server and config
    • default pipeline remains unchanged when flag is omitted
    • bounded runtime policy is enforced from a single control point:
      • max tool count, max probe payload count, max payload fields, per-probe timeout
    • dynamic findings are returned in deterministic order with stable metadata keys
    • benign placeholder/blocked-execution contexts are suppressed to reduce false positives
  • Refresh fallback behavior:
    • if refresh fails with invalid_grant / invalid_token, scanner drops cached refresh token and retries primary grant once
    • if retry requires interaction in headless mode, auth_token_error is emitted and scan continues
  • auth finding evidence never includes secret/token/cookie plaintext
  • Unsupported transport entries do not stop the run; they are reported as findings
  • Per-server scan failures do not stop the run; they are reported as scan_failure findings
  • URL positional commands (server, baseline, compare) support:
    • --headers-json (JSON object)
    • --auth-json (JSON object with same shape as config.auth)
    • --mtls-cert-file + --mtls-key-file (optional --mtls-ca-bundle-file)
  • URL auth/mTLS options are URL-only; when used with stdio targets the command fails with operational error (exit 2)

cache command:

  • mcp-scan cache rotate rotates persistent OAuth cache encryption key and re-encrypts cached entries
  • exit 0 on success, exit 2 on operational failure

Outputs and Severity Filter

  • --format: json (default), html, sarif
  • --output: write report to file; if omitted, prints to stdout
  • --severity: critical | high | medium | low | info | all

Severity threshold is inclusive (high shows high + critical).

Exit Codes

Command Exit 0 Exit 1 Exit 2
server No findings after severity filter Findings exist after filter Operational error
config No findings after severity filter Findings exist after filter Operational error
compare No findings after severity filter Findings exist after filter Operational error
baseline Baseline created successfully N/A Operational error

Baseline v1 Format

baseline writes a baseline-v1 JSON document:

  • schema_version
  • scanner_version
  • created_at
  • server (name, command)
  • tools[] (overall_hash, field hashes, metadata)

compare maps mutation severity as:

  • removed / changed: high
  • added: medium

All mutation findings map to OWASP: LLM05.

Development

pytest -q
mypy src

Current quality gate:

  • tests passing
  • coverage >=80%
  • mypy src clean

Roadmap (Post Sprint 8AC)

Deferred items:

  • additional persistent secret-store providers beyond local, aws_secrets_manager, aws_ssm_parameter_store, gcp_secret_manager, azure_key_vault, hashicorp_vault, kubernetes_secrets, oci_vault, doppler_secrets, onepassword_connect, bitwarden_secrets, infisical_secrets, akeyless_secrets, gitlab_variables, gitlab_group_variables, github_actions_variables, github_environment_variables, github_organization_variables, consul_kv, redis_kv, and cloudflare_kv; backend onboarding now uses a shared dispatch/contract baseline from Sprint 8AA

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ogulcanaydogan_mcp_security_scanner-0.1.25.tar.gz (166.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

File details

Details for the file ogulcanaydogan_mcp_security_scanner-0.1.25.tar.gz.

File metadata

File hashes

Hashes for ogulcanaydogan_mcp_security_scanner-0.1.25.tar.gz
Algorithm Hash digest
SHA256 6319a853ff538c61c67fc2150ad236d659e495bd662bfeb84cda6ad9174535e6
MD5 34119fc4a6cae6c89c04ec6d44f8f385
BLAKE2b-256 85aa11312219591c7d7483728ca37961fe3647fe1b72664cb89097d8d9599f38

See more details on using hashes here.

Provenance

The following attestation bundles were made for ogulcanaydogan_mcp_security_scanner-0.1.25.tar.gz:

Publisher: ci.yml on ogulcanaydogan/mcp-security-scanner

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file ogulcanaydogan_mcp_security_scanner-0.1.25-py3-none-any.whl.

File metadata

File hashes

Hashes for ogulcanaydogan_mcp_security_scanner-0.1.25-py3-none-any.whl
Algorithm Hash digest
SHA256 663c4a60aea48ae1ca809afb948d764d7a5e35fccf2f233060530eb78b18318e
MD5 a509f450bbd73a23fedd5e5833f32704
BLAKE2b-256 92e052ed6ea7e93b8a191804cc96c2d7aa87b851ef574db950742e3ff447f97a

See more details on using hashes here.

Provenance

The following attestation bundles were made for ogulcanaydogan_mcp_security_scanner-0.1.25-py3-none-any.whl:

Publisher: ci.yml on ogulcanaydogan/mcp-security-scanner

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page