[project]
name = "mcp-context-protector"
dynamic = ["version"]
description = "MCP security wrapper"
readme = "README.md"
license-files = ["LICENSE"]
license = "Apache-2.0"
authors = [
{ name = "Trail of Bits", email = "opensource@trailofbits.com" },
]
classifiers = [
"Programming Language :: Python :: 3",
]
dependencies = [
"mcp==1.9.4",
"llamafirewall>=1.0.3",
"aiofiles>=24.1.0",
]
requires-python = ">=3.11"
[tool.setuptools.dynamic]
version = { attr = "contextprotector.__version__" }
[project.scripts]
"mcp-context-protector" = "contextprotector.__main__:main"
[project.urls]
Homepage = "https://github.com/trailofbits/mcp-context-protector/"
Documentation = "https://github.com/trailofbits/mcp-context-protector/README.md"
Issues = "https://github.com/trailofbits/mcp-context-protector/issues"
Source = "https://github.com/trailofbits/mcp-context-protector"
[tool.coverage.run]
# don't attempt code coverage for the CLI entrypoints
omit = ["src/contextprotector/__main__.py"]
[tool.mypy]
mypy_path = "src"
packages = "contextprotector"
allow_redefinition = true
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_untyped_defs = true
ignore_missing_imports = true
no_implicit_optional = true
show_error_codes = true
sqlite_cache = true
strict_equality = true
warn_no_return = true
warn_redundant_casts = true
warn_return_any = true
warn_unreachable = true
warn_unused_configs = true
warn_unused_ignores = true
# Suppress errors since pydantic_settings uses X | Y typing
python_version = "3.11"
plugins = ["pydantic.mypy"]
[tool.ruff]
line-length = 100
include = ["src/**/*.py", "test/**/*.py"]
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"ARG", # flake8-unused-arguments
"SIM", # flake8-simplify
"TID", # flake8-tidy-imports
"RUF", # ruff-specific rules
"D", # pydocstyle
"S", # flake8-bandit
"T20", # flake8-print
"N", # pep8-naming
"PL", # pylint
]
# D203 and D213 are incompatible with D211 and D212 respectively.
ignore = ["D203", "D213", "C901", "PLR0912", "PLR0915"]
[tool.ruff.lint.per-file-ignores]
"**/*cli*.py" = [
"T201", # allow `print` in cli module
]
"test/**/*.py" = [
"D", # no docstrings in tests
"S101", # asserts are expected in tests
"T201", # allow `print` in tests
"PLR2004", # allow "magic numbers" in tests
"SLF001", # allow private member access in tests
"PLW0603", # allow global variables
"PLW0602", # allow global variables
"PLC0415", # allow imports inside functions in tests
"SIM115", # allow NamedTemporaryFile without context manager
"S603", # allow subprocess calls in tests
"UP031", # allow percent formatting in tests
]
"src/contextprotector/__main__.py" = [
"T201", # allow `print` in main module
]
"src/contextprotector/mcp_wrapper.py" = [
"PLC0415", # allow lazy imports for optional MCP client libraries
]
"src/contextprotector/wrapper_config.py" = [
"PLC0415", # allow lazy imports to avoid circular dependencies
]
[tool.interrogate]
# don't enforce documentation coverage for packaging, testing, the virtual
# environment, or the CLI (which is documented separately).
exclude = ["env", "test", "src/contextprotector/__main__.py"]
ignore-semiprivate = true
fail-under = 100
[tool.pytest.ini_options]
asyncio_default_fixture_loop_scope = "function"
testpaths = ["test"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
[dependency-groups]
test = [
"pytest>=8.4.1",
"pytest-asyncio>=1.1.0",
"pytest-timeout>=2.4.0",
"pytest-cov",
"pretend",
"coverage[toml]",
]
lint = [
"ruff >= 0.12.7", # Use latest ruff for improved checks
"interrogate",
]
doc = [
"pdoc",
]
dev = [
"psutil>=7.0.0",
"twine",
"build",
]
[tool.uv]
package = true
managed = true