[build-system] requires = ["hatchling>=1.25", "hatch-vcs>=0.4"] build-backend = "hatchling.build" [project] name = "signnow-mcp-server" dynamic = ["version"] description = "A Model Context Protocol (MCP) server that provides SignNow API integration capabilities." readme = "README.md" requires-python = ">=3.10" dependencies = [ "fastmcp>=3.1.0,<4", "uvicorn[standard]>=0.24", "typer>=0.9", "mcp[cli]>=1.27.1,<2", "httpx[http2]>=0.25", "pyjwt[crypto]>=2.8", "cryptography>=42", "starlette>=0.27", "pydantic>=2.0", "pydantic-settings>=2.0", "mcp-ui-server>=1.0.0", ] [project.scripts] sn-mcp = "sn_mcp_server.cli:app" [project.optional-dependencies] smolagents = [ "smolagents>=1.20", "mcpadapt>=0.1.11", ] llamaindex = [ "llama-index-tools-mcp>=0.1.0", "llama-index-core>=0.10.0", "llama-index-llms-openai>=0.1.0", "python-dotenv>=1.0.0", ] langchain = [ "langchain>=0.1.0", "langchain-openai>=0.1.0", "langchain-mcp-adapters>=0.1.0", "langchain-core>=0.1.0", ] test = [ "pytest>=7.0", "pytest-asyncio>=0.21", "pytest-mock>=3.10", "httpx>=0.25", "respx>=0.22", "pytest-cov>=5.0", "pytest-httpserver>=1.0", "smolagents[mcp]>=1.20", ] dev = [ "signnow-mcp-server[test]", "pre-commit>=3.7", "ruff>=0.7", # 1.18+ split `untyped-decorator` from `misc`; tools-layer override in # [tool.mypy] disables both codes, so older mypy would silently drop # the whole list and start flagging FastMCP decorators. "mypy>=1.18", "import-linter>=2.0", "diff-cover>=9.0", ] [tool.coverage.run] source = ["src"] omit = [ "src/sn_mcp_server/_version.py", "src/*/migrations/*", ] branch = true [tool.coverage.report] show_missing = true skip_covered = false # Minimum total coverage. Current baseline is ~75% (2026-04-20). Ratchet up # as untested legacy code gets covered, don't let it slide down. CI and # pre-push both honor this via pytest --cov-fail-under (passed on CLI to # make failures obvious in logs). fail_under = 70 exclude_lines = [ "pragma: no cover", "if TYPE_CHECKING:", "raise NotImplementedError", ] [tool.ruff] line-length = 200 lint.select = ["E","F","W","I","N","B","UP","S","ASYNC","ANN"] format.preview = true # _version.py is regenerated by hatch-vcs on every install with its own # formatting (single quotes, etc.). It's gitignored — never in the tree. extend-exclude = ["src/sn_mcp_server/_version.py"] [tool.ruff.lint.per-file-ignores] "tests/**/*.py" = ["S101"] # assert is idiomatic in pytest # Introspects FastMCP's `@mcp.tool` decorator to capture registered tools; # the wrapper is a generic callable, so Any is the only accurate annotation. "tests/unit/sn_mcp_server/test_tool_response_shapes.py" = ["S101", "ANN401"] # Same reason as above: recording wrappers for mcp.tool interception require Any. "tests/unit/sn_mcp_server/test_tool_versions.py" = ["S101", "ANN401"] "tests/unit/sn_mcp_server/test_write_tool_freshness_descriptions.py" = ["S101", "ANN401"] "tests/unit/sn_mcp_server/tools/test_signnow_v1.py" = ["S101", "ANN401"] "tests/unit/sn_mcp_server/tools/test_create_from_template.py" = ["S101", "ANN401"] "tests/unit/sn_mcp_server/tools/test_document.py" = ["S101", "ANN401"] "src/sn_mcp_server/auth.py" = ["S101"] # unreachable type-narrowing asserts; see auth.py:181,191,230 # Pydantic field_validator(mode="before") helpers normalize raw API payloads of unknown shape; # Any is the correct input type — the helper's job is to narrow it to a concrete type. "src/signnow_client/models/**/*.py" = ["ANN401"] "eval/**/*.py" = ["ANN401"] [tool.mypy] python_version = "3.10" show_error_codes = true strict = true warn_unused_ignores = true plugins = ["pydantic.mypy"] [[tool.mypy.overrides]] module = ["tests.unit.*", "tests.integration.*", "tests.api.*", "tests.e2e.*", "tests.conftest"] disable_error_code = ["misc"] # FastMCP 3.x ships @mcp.tool / @mcp.prompt / @mcp.resource as untyped decorators # (their overloads use Callable[..., Any] returns), so every registered handler # trips mypy's "Untyped decorator makes function ... untyped" check. Older # mypy emitted this under [misc]; newer versions split it into # [untyped-decorator]. Silence both so the override works regardless of the # mypy version installed locally vs in CI. We can't fix FastMCP's typing # from here; scope the silence to the tools layer. [[tool.mypy.overrides]] module = ["sn_mcp_server.tools.*"] disable_error_code = ["misc", "untyped-decorator"] [tool.hatch.version] source = "vcs" tag-pattern = "^v?(?P.*)$" [tool.hatch.build.hooks.vcs] version-file = "src/sn_mcp_server/_version.py" [tool.hatch.build.targets.wheel] packages = [ "src/sn_mcp_server", "src/signnow_client", ] sources = ["src"] # Architectural layer boundaries — the Python equivalent of # eslint-plugin-boundaries. Agents can't be relied on to hold the layer # rules from AGENTS.md in their head on every edit, so encode them here # and fail the pre-push / CI run when crossed. # # Layers (top = closer to MCP protocol, bottom = closer to HTTP): # # tools user-facing MCP tool definitions (fastmcp decorators, prompt # descriptions). Orchestrates. Never talks to Starlette/httpx # directly. # client signnow_client — SignNow API wrapper. httpx lives here. # Has no idea MCP exists. # auth sn_mcp_server/auth.py + auth/ — OAuth + JWT middleware. # Talks to Starlette. # app sn_mcp_server/app.py + server.py — ASGI app wiring. # Talks to Starlette. # # The one-way arrow that matters most: sn_mcp_server depends on # signnow_client, never the other way. That's rule #1 from AGENTS.md. [tool.importlinter] root_packages = ["sn_mcp_server", "signnow_client"] # Needed because the `tools must not import starlette/uvicorn` contract # names external packages as forbidden targets. include_external_packages = true [[tool.importlinter.contracts]] name = "signnow_client must not import sn_mcp_server" type = "forbidden" source_modules = ["signnow_client"] forbidden_modules = ["sn_mcp_server"] [[tool.importlinter.contracts]] name = "tool modules must not import transport/ASGI machinery directly" type = "forbidden" source_modules = ["sn_mcp_server.tools"] forbidden_modules = ["starlette", "uvicorn"]