[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "swisstopo-mcp" version = "0.4.1" description = "MCP server for Swiss federal geodata (Swisstopo APIs)" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.11" keywords = [ "mcp", "model-context-protocol", "swiss-open-data", "swisstopo", "geodata", "gis", "geocoding", ] classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering :: Artificial Intelligence", ] dependencies = [ # Pinned to one major so an SDK update cannot silently change the negotiated # MCP protocol version (ARCH-012). Dependabot proposes bumps. # Now the 2.x major (`mcp.server.mcpserver`). The floor is hard: 2.0.0 # removed `mcp.server.fastmcp` with no compatibility shim, so this code # cannot run on 1.x — a `>=1.x` range would let a resolver pick a version # that fails at import. # 2.x serves two protocol eras: the legacy `initialize` handshake caps at # 2025-11-25 (what the READMEs document), the modern envelope era reaches # 2026-07-28. `tests/test_protocol_version.py` pins both, and measures the # handshake ceiling against a live server rather than a constant name. "mcp[cli]>=2.0.0,<3", "httpx>=0.27.0", "pydantic>=2.0.0", "pydantic-settings>=2.0.0", "structlog>=24.1.0,<27", # OBS-006: tracing is inert unless OTEL_EXPORTER_OTLP_ENDPOINT is set, but # the packages ship by default so a deployment needs no separate install. "opentelemetry-api>=1.27.0", "opentelemetry-sdk>=1.27.0", "opentelemetry-exporter-otlp>=1.27.0", "opentelemetry-instrumentation-httpx>=0.48b0", ] [project.optional-dependencies] dev = [ "pytest>=8.0.0", "pytest-asyncio>=0.23.0", "pytest-cov>=5.0.0", "respx>=0.21.0", # Exakt gepinnt, und die einzige Stelle, an der die ruff-Version steht. # # Die Begruendung der bisherigen Deckelung bleibt gueltig: ruff ist pre-1.0, # Regelverhalten und neue Checks landen in den Minors, und ein Release soll # die CI nicht ohne Zutun rot machen. Dependabot schlaegt den Bump auch bei # `==` vor, das Review-Gate bleibt also erhalten — ein Pin erfuellt diesen # Zweck strenger als eine Spanne, weil er auch innerhalb von 0.16.x nicht # driftet. # # Die Spanne liess `pip install -e ".[dev]"` auf 0.16.3 aufloesen, waehrend # ci.yml `ruff==0.16.4` darueber installierte: lokaler Lauf und Gate waren # sich also ueber die Version uneinig. Der CI-Schritt ist entfallen. "ruff==0.16.4", # Ebenfalls exakt, aus demselben Grund. `mypy src/` ist ein Gate, mypy war # aber als `mypy>=1.10.0` voellig offen — das Argument ueber dem ruff-Pin # galt hier genauso und wurde nicht angewandt. 2.3.1 ist die Version, unter # der `mypy src/` am 16.08.2026 gruen ist. "mypy==2.3.1", # Test-only: the egress-ACL test parses deploy/smokescreen-acl.yaml the way # the proxy would (SEC-021). Not a runtime dependency — the server itself # reads no YAML. It was available transitively in the dev sandbox, which is # why the gap only surfaced in CI. "PyYAML>=6.0", # Test-only: the protocol-boundary tests validate structuredContent against # each tool's outputSchema (OBS-001). It arrives transitively via mcp, but # depending on that is how the PyYAML gap reached CI. "jsonschema>=4.0", ] [project.scripts] swisstopo-mcp = "swisstopo_mcp.server:mcp.run" [project.urls] Homepage = "https://github.com/malkreide/swisstopo-mcp" Repository = "https://github.com/malkreide/swisstopo-mcp" Issues = "https://github.com/malkreide/swisstopo-mcp/issues" [tool.hatch.build.targets.wheel] packages = ["src/swisstopo_mcp"] [tool.pytest.ini_options] asyncio_mode = "auto" testpaths = ["tests"] addopts = "-v --tb=short" markers = [ "live: live API tests (skipped in CI by default)", ] [tool.mypy] # Static gate for SDK-002: four handlers were annotated `-> str` while returning # ToolResponse, and nothing caught it. Deliberately permissive to start — the # point is to freeze the return-type contract, not to boil the ocean. files = ["src"] python_version = "3.11" check_untyped_defs = true warn_return_any = true warn_redundant_casts = true ignore_missing_imports = true [tool.ruff] line-length = 100 target-version = "py311" [tool.ruff.lint] select = ["E", "F", "W", "I", "UP"] ignore = ["E501"]