Security scanner for Model Context Protocol (MCP) servers. Detects prompt injection, tool poisoning, capability escalation, and rug-pull attacks.
Project description
MCP Security Scanner
Security scanner for Model Context Protocol (MCP) servers.
Scans MCP capabilities, runs analyzer checks, and exports findings in json, html, or sarif.
Current Scope (Sprint 1-8F)
stdio,sse, andstreamable-httptransport support in discovery/connector layer- CLI commands implemented:
server,config,baseline,compare,cache rotate configsupports 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)
- Advanced persistent OAuth cache backend v1 (
auth.cache.backend=aws_secrets_manager) for config-based OAuth flows - OAuth provider hardening+ (tolerant token parsing and transient retry policy for token endpoints)
- OAuth provider integrations v2 in
configauth:token_endpoint_auth_method=private_key_jwtsupports env/file/AWS KMS signing sources - OAuth token-endpoint mTLS (
auth.mtls_*) and transport-level discovery mTLS (mtls_*on network entries) - Dynamic analyzer hardening (opt-in
--dynamic) with bounded probe policy, deterministic ordering, and noise suppression - Dynamic analyzer expansion (opt-in
--dynamic) with semantic probe variants and stronger false-positive suppression - Release stabilization (Sprint 8D): PyPI distribution name switched to
ogulcanaydogan-mcp-security-scannerto avoid name collision - Default analyzers enabled in scan flows:
StaticAnalyzerPromptInjectionAnalyzerEscalationAnalyzerToolPoisoningAnalyzerCrossToolAnalyzer
- 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]
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-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:
stdiovalidation:commandrequired,argsoptional list,envoptional objectssevalidation:urlrequired (http/https),headersoptional objectstreamable-httpvalidation:urlrequired (http/https),headersoptional objecttransport: "streamable_http"alias is accepted and normalized tostreamable-httpauthis optional and only valid forsse/streamable-httpentriesauthvalidation/env resolution errors produceauth_config_errorfindings and scan continues with remaining servers- OAuth token endpoint/network/response failures produce
auth_token_errorfindings and scan continues with remaining servers oauth_client_credentialsandoauth_device_codesupport optionaltoken_endpoint_auth_method:client_secret_post(default)client_secret_basic(oauth_device_coderequiresclient_secret_envwhen used)private_key_jwt(oauth_client_credentials+oauth_device_code;oauth_auth_code_pkceremains unchanged)
private_key_jwtvalidation rules:- exactly one signing source is required:
client_assertion_key_envclient_assertion_key_fileclient_assertion_kms_key_id(AWS KMS signing)
- optional KMS tuning:
client_assertion_kms_region,client_assertion_kms_endpoint_url - optional
client_assertion_kidis propagated into JWT header - v1 signing algorithm is
RS256
- exactly one signing source is required:
- token endpoint mTLS options for OAuth auth entries:
mtls_cert_file+mtls_key_filemust be provided together- optional
mtls_ca_bundle_fileis 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_filemust be provided together - optional top-level
mtls_ca_bundle_fileis used as connection verify bundle - applies to discovery transport HTTP client setup (independent from
auth.mtls_*)
- top-level
- OAuth token cache key is deterministic:
namespace + token_url + client_id + scope + audience auth.cacheis optional and only valid for OAuth auth types:persistent(bool, defaultfalse)namespace(string, default"default")backend(string, default"local"):localoraws_secrets_manageraws_secret_id(required whenbackend=aws_secrets_manager)- optional
aws_region,aws_endpoint_urlfor AWS client routing
- 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+historicalkey entries (key_id+fernet_key); legacy raw key format remains readable - decrypt recovery order: payload
key_idmatch when possible, then active key, then historical keys (deterministic order) - historical key retention is bounded (max 3);
cache rotatepromotes current active key into historical set - fallback key file is created with
0600permissions - cache/key file mode hardening uses best-effort
0600 - corrupt or undecryptable cache payloads are quarantined as
oauth-cache-v1.json.enc.corrupt.<timestamp>
- encrypted file:
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_regionandaws_endpoint_urltune client resolution
- cache payload is stored as a single JSON envelope in the configured AWS secret (
- backend read/write/decrypt/parse failures are non-fatal; scanner falls back to live token flow
oauth_device_codeuses copy/paste UX (verification_uri+user_code) and supports refresh-token reuse on expiry- in headless/CI environments (no interactive TTY),
oauth_device_codeentries produceauth_token_errorand scan continues oauth_auth_code_pkceuses local callback + PKCE (S256), prints authorization URL, and supports refresh-token reuse on expiryoauth_auth_code_pkcecallback 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_pkceentries produceauth_token_errorand 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
2retries (total3attempts), short bounded backoff
- retryable statuses:
- dynamic analyzer v1 is opt-in:
- enable with
--dynamiconserverandconfig - 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
- enable with
- 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_erroris emitted and scan continues
- if refresh fails with
- 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_failurefindings - URL positional commands (
server,baseline,compare) support:--headers-json(JSON object)--auth-json(JSON object with same shape asconfig.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 rotaterotates persistent OAuth cache encryption key and re-encrypts cached entries- exit
0on success, exit2on 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_versionscanner_versioncreated_atserver(name,command)tools[](overall_hash, field hashes, metadata)
compare maps mutation severity as:
removed/changed:highadded:medium
All mutation findings map to OWASP: LLM05.
Development
pytest -q
mypy src
Current quality gate:
- tests passing
- coverage
>=80% mypy srcclean
Roadmap (Post Sprint 8E)
Deferred items:
- additional persistent secret-store providers beyond
localandaws_secrets_manager
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ogulcanaydogan_mcp_security_scanner-0.1.1.tar.gz.
File metadata
- Download URL: ogulcanaydogan_mcp_security_scanner-0.1.1.tar.gz
- Upload date:
- Size: 109.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f964a4de056a5d69b3477e7be9f049f52ed24471af08a3177d120e98ac470e44
|
|
| MD5 |
73c43f51900a9b8edea3b89b13a2ea73
|
|
| BLAKE2b-256 |
149c9485a7a565625d08c4ddd18b72f1606cece6fbe30f455d05dda98de9b2e6
|
Provenance
The following attestation bundles were made for ogulcanaydogan_mcp_security_scanner-0.1.1.tar.gz:
Publisher:
ci.yml on ogulcanaydogan/mcp-security-scanner
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ogulcanaydogan_mcp_security_scanner-0.1.1.tar.gz -
Subject digest:
f964a4de056a5d69b3477e7be9f049f52ed24471af08a3177d120e98ac470e44 - Sigstore transparency entry: 1145816298
- Sigstore integration time:
-
Permalink:
ogulcanaydogan/mcp-security-scanner@db16c4775c8bd9d92d4da28069eb2df46edd078e -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/ogulcanaydogan
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@db16c4775c8bd9d92d4da28069eb2df46edd078e -
Trigger Event:
push
-
Statement type:
File details
Details for the file ogulcanaydogan_mcp_security_scanner-0.1.1-py3-none-any.whl.
File metadata
- Download URL: ogulcanaydogan_mcp_security_scanner-0.1.1-py3-none-any.whl
- Upload date:
- Size: 74.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
366a7594367b285b01cdaa34bfdecea6d6ffbd2b7a8728fa4b280ece91c3599b
|
|
| MD5 |
0d125e2dbb8c5fe814bcc252ab57abc4
|
|
| BLAKE2b-256 |
9d25cb5efd84e976d497d9fa21200342deb4e3e1e30030bb8174f61d7044ad47
|
Provenance
The following attestation bundles were made for ogulcanaydogan_mcp_security_scanner-0.1.1-py3-none-any.whl:
Publisher:
ci.yml on ogulcanaydogan/mcp-security-scanner
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ogulcanaydogan_mcp_security_scanner-0.1.1-py3-none-any.whl -
Subject digest:
366a7594367b285b01cdaa34bfdecea6d6ffbd2b7a8728fa4b280ece91c3599b - Sigstore transparency entry: 1145816374
- Sigstore integration time:
-
Permalink:
ogulcanaydogan/mcp-security-scanner@db16c4775c8bd9d92d4da28069eb2df46edd078e -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/ogulcanaydogan
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@db16c4775c8bd9d92d4da28069eb2df46edd078e -
Trigger Event:
push
-
Statement type: