[project] name = "mcpg" # Version is single-sourced from src/mcpg/__init__.py (__version__) and read # here dynamically by hatchling — see [tool.hatch.version]. A release bumps # exactly one line, so pip, `mcpg --version`, and the MCP serverInfo # handshake can never disagree. dynamic = ["version"] description = "A production-grade PostgreSQL Model Context Protocol (MCP) server." readme = "README.md" requires-python = ">=3.12" license = "MIT" license-files = ["LICENSE"] authors = [ {name = "Devopam Mittra", email = "devopam@gmail.com"}, ] maintainers = [ {name = "Devopam Mittra", email = "devopam@gmail.com"}, ] keywords = [ "postgresql", "postgres", "mcp", "model-context-protocol", "ai-agents", "llm-tools", "database", "claude", "anthropic", "dba", "sql", "rag", ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: SQL", "Topic :: Database", "Topic :: Database :: Database Engines/Servers", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Systems Administration", "Typing :: Typed", ] dependencies = [ "mcp[cli]>=2.0.0", "psycopg[binary]>=3.3.2", "psycopg-pool>=3.3.0", "pglast==8.4", "typing-extensions>=4.12", # httpx ships transitively via mcp; declare it explicitly so the # NL→SQL providers in mcpg.nl2sql have a guaranteed surface. "httpx>=0.27", # mcp-types ships transitively via mcp; declare it explicitly so # mcpg.dynamic_session_intent has a guaranteed surface. "mcp-types>=2.0.0", # JWT validation for the OIDC auth path. ``[crypto]`` pulls in # ``cryptography`` so RS256 / ES256 signature checks work. "pyjwt[crypto]>=2.8", "circuitbreaker>=2.1.3", "tenacity>=9.1.4", ] # Optional extras the runtime picks up only on demand. ``otel`` adds # OpenTelemetry tracing — one span per ``call_tool`` invocation — # behind the ``MCPG_OTEL_ENABLED`` flag. The api/sdk pair are the # trace machinery; the exporter ships traces to any OTLP-compatible # collector over HTTP/protobuf. [project.optional-dependencies] otel = [ "opentelemetry-api>=1.27", "opentelemetry-sdk>=1.27", "opentelemetry-exporter-otlp-proto-http>=1.27", ] # Cloud secrets backends — selected by ``MCPG_SECRETS_BACKEND=vault|aws|gcp``. # Each is its own extra so a deployment doesn't pay for every cloud SDK # when it only uses one. The SDKs are imported lazily inside the # provider class, so the import / install error is surfaced with a # clear "install mcpg[xxx]" message on first lookup if the operator # selected a backend but forgot the extra. vault = ["hvac>=1.0"] aws = ["boto3>=1.28"] gcp = ["google-cloud-secret-manager>=2.16"] # Powers the "Project links" sidebar on https://pypi.org/project/mcpg/. # Order is preserved on the page; first entry doubles as the page header # "Homepage" link. [project.urls] Homepage = "https://github.com/devopam/MCPg" Documentation = "https://github.com/devopam/MCPg#documentation" Repository = "https://github.com/devopam/MCPg" Issues = "https://github.com/devopam/MCPg/issues" Changelog = "https://github.com/devopam/MCPg/blob/main/CHANGELOG.md" "Release notes" = "https://github.com/devopam/MCPg/releases" Security = "https://github.com/devopam/MCPg/blob/main/SECURITY.md" [project.scripts] mcpg = "mcpg.__main__:main" [build-system] requires = ["hatchling>=1.26"] build-backend = "hatchling.build" # Single source of truth for the version: hatchling reads ``__version__`` # from this file at build time (its built-in regex version source), so the # project version, ``mcpg --version``, and the MCP serverInfo handshake all # derive from one line. Bump src/mcpg/__init__.py and nothing else. [tool.hatch.version] path = "src/mcpg/__init__.py" [tool.hatch.build.targets.wheel] packages = ["src/mcpg"] include = ["src/mcpg/py.typed"] # Keep the sdist tight — ship the source tree + the prose a user # actually wants when they unpack from PyPI, and leave dev-only # noise (tests / CI / IDE / VCS cruft) on GitHub. [tool.hatch.build.targets.sdist] include = [ "src/mcpg", "README.md", "LICENSE", "NOTICE", "CHANGELOG.md", "SECURITY.md", "pyproject.toml", ] [dependency-groups] dev = [ "pytest>=9.0.2", "pytest-asyncio>=1.3.0", "pytest-cov>=6.0.0", "docker>=7.1.0", "ruff>=0.14.0", "mypy>=1.15.0", "pre-commit>=4.0.0", "bandit>=1.7.8", "pip-audit>=2.7", # PyPI packaging toolchain — build the sdist/wheel, lint the # rendered metadata, push to PyPI in the publish workflow. "build>=1.2", "twine>=5.1", # CycloneDX SBOM generation for releases (supply-chain transparency). # Run in the publish workflow to attach an SBOM to the GitHub release. "cyclonedx-bom>=4.0", # OpenTelemetry SDK + OTLP HTTP exporter — pinned in dev so the # otel-tracing unit tests can use the InMemorySpanExporter and so # `mcpg[otel]` round-trips through CI. Production deployments # opt in via the ``mcpg[otel]`` extra rather than dev install. "opentelemetry-api>=1.27", "opentelemetry-sdk>=1.27", "opentelemetry-exporter-otlp-proto-http>=1.27", "pip-licenses>=5.5.5", "pytest-mock>=3.15.1", "pytest-randomly>=4.1.0", "pytest-socket>=0.8.1", "time-machine>=3.5.0", "pytest-rerunfailures>=16.6", ] # Benchmark suite (roadmap 19) — dev-only. Keeps the shipped package lean; # `duckdb` generates the TPC-H dataset for benchmarks/datasets/load_tpch.py, # `tiktoken` counts tokens for the Tier-A token-efficiency accounting # (benchmarks/tokens/), and `anthropic` drives the costed Tier-B agent-loop # study (benchmarks/tokens/tier_b/ — never run in CI). bench = [ "duckdb>=1.1", "tiktoken>=0.7", "anthropic>=0.40", ] [tool.pytest.ini_options] # "." so the root-level ``benchmarks`` package (roadmap 19) is importable in # its unit tests; "src" for mcpg, "tests" for the _fakes helpers. pythonpath = ["src", "tests", "."] testpaths = ["tests"] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" markers = [ "integration: requires a live PostgreSQL (set MCPG_TEST_DATABASE_URL)", ] filterwarnings = [ # pytest resets warnings.filters per test, so the runtime filter # mcpg/__init__.py installs (same pattern, kept in sync manually) # doesn't survive into test collection — repeat it here so the ~180 # tool-return dataclasses with a `schema` field don't spam every test # that touches Tool.output_schema. 'ignore:Field name ".*" in ".*" shadows an attribute in parent "BaseModel":UserWarning', ] [tool.coverage.run] source = ["src/mcpg"] [tool.coverage.report] # Coverage gate applies to all authored code — the SQL kernel is first-party. fail_under = 90 show_missing = true [tool.ruff] line-length = 120 target-version = "py312" extend-exclude = ["scratch", "demo.py"] # Honour the exclusions even when files are passed explicitly (e.g. pre-commit). force-exclude = true [tool.ruff.lint] select = ["E", "F", "I", "B", "W", "N", "UP", "RUF", "C90", "ASYNC", "C4", "SIM", "PTH", "PYI", "FBT"] [tool.ruff.lint.isort] known-first-party = ["mcpg"] [tool.mypy] python_version = "3.14" strict = true exclude = ["scratch/"] [[tool.mypy.overrides]] # pglast ships partial type hints (as of v8); ignore import-resolution gaps # for the submodules we don't have full types for. module = ["pglast", "pglast.*"] ignore_missing_imports = true [[tool.mypy.overrides]] # circuitbreaker (audit remediation Task 15) ships no py.typed marker and no # stub package — `@circuit` therefore types as `Any`, which on its own would # only need ignore_missing_imports. The decorated methods still need a # `# type: ignore[untyped-decorator]` at each call site (see nl2sql.py / # oidc.py) because --strict's disallow_untyped_decorators fires independently # of import resolution. module = ["circuitbreaker"] ignore_missing_imports = true [[tool.mypy.overrides]] # Optional cloud-secrets SDKs ship without typed stubs we want to lean # on — the providers in mcpg.secrets import them lazily and convert # everything to plain ``str | None`` before re-exposing the API. module = [ "hvac", "hvac.*", "boto3", "boto3.*", "botocore", "botocore.*", "google.cloud", "google.cloud.*", "google.api_core", "google.api_core.*", ] ignore_missing_imports = true [tool.bandit] exclude_dirs = ["tests"] skips = ["B101", "B608", "B110"]