[project] name = "laserfiche-mcp" version = "2.3.0" description = "Model Context Protocol (MCP) server for self-hosted Laserfiche repositories — search and read documents from Claude" readme = "README.md" requires-python = ">=3.10" license = { text = "MIT" } authors = [ { name = "Samuel S. Hernandez" }, ] keywords = [ "mcp", "model-context-protocol", "laserfiche", "claude", "anthropic", "ecm", "document-management", "records-management", ] classifiers = [ "Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "Intended Audience :: Legal Industry", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Communications", "Topic :: Office/Business", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed", ] dependencies = [ "mcp[cli]>=1.2.0,<2", "httpx>=0.27.0,<1", "pydantic>=2.0.0,<3", "pydantic-settings>=2.0.0,<3", "pypdf>=4.0.0,<7", ] [project.optional-dependencies] # OAuth Resource Server mode for the --http transport (per-user token # verification). Optional: `pip install 'laserfiche-mcp[oauth]'`. Only needed # when LF_HTTP_OAUTH_ISSUER is configured. oauth = [ "pyjwt[crypto]>=2.9.0,<3", ] # Extra document formats for `cat` / `find` / mode="text". The base install # reads PDF, DOCX, PPTX, EML, HTML, RTF and text/* with no extra dependencies; # these two cover the remaining common cases. office = [ "openpyxl>=3.1,<4", # .xlsx "extract-msg>=0.48,<1", # Outlook .msg ] dev = [ "pytest>=8.0.0", "pytest-asyncio>=0.23.0", "pytest-cov>=5.0.0", "pytest-httpx>=0.30.0", "ruff>=0.5.0", "mypy>=1.10.0", # OAuth extra is exercised by the test suite, so CI (uv sync --extra dev) # must have it available. "pyjwt[crypto]>=2.9.0,<3", ] [project.scripts] laserfiche-mcp = "laserfiche_mcp.server:main" [project.urls] Homepage = "https://github.com/SamuelSHernandez/laserfiche-mcp" Repository = "https://github.com/SamuelSHernandez/laserfiche-mcp" Documentation = "https://github.com/SamuelSHernandez/laserfiche-mcp#readme" Changelog = "https://github.com/SamuelSHernandez/laserfiche-mcp/blob/main/CHANGELOG.md" Issues = "https://github.com/SamuelSHernandez/laserfiche-mcp/issues" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.sdist] # Internal working notes (audit/plan scratch) don't belong in the published # source distribution. exclude = ["docs/internal"] [tool.hatch.build.targets.wheel] packages = ["src/laserfiche_mcp"] # Ensure the PEP 561 marker ships in the wheel so downstream type checkers # treat the package as typed. [tool.hatch.build.targets.wheel.force-include] "src/laserfiche_mcp/py.typed" = "laserfiche_mcp/py.typed" [tool.ruff] line-length = 100 target-version = "py310" [tool.ruff.lint] select = ["E", "F", "I", "N", "W", "UP", "B", "SIM", "RET"] [tool.ruff.format] # Match ruff's default style explicitly so CI's `ruff format --check` has a # stable target across ruff versions. quote-style = "double" indent-style = "space" line-ending = "lf" skip-magic-trailing-comma = false [tool.mypy] # Gate enforced in CI. To run locally: `uv run mypy`. python_version = "3.10" files = ["src"] disallow_untyped_defs = true check_untyped_defs = true warn_unused_ignores = true warn_redundant_casts = true warn_return_any = true no_implicit_optional = true ignore_missing_imports = true [tool.pytest.ini_options] asyncio_mode = "auto" testpaths = ["tests"] markers = [ "integration: hits a real Laserfiche Repository API. Opt in by setting LF_INTEGRATION_TEST=1.", ] # pytest-cov is wired through addopts so `uv run pytest` enforces the # baseline; opt out for a single run with `--no-cov` if you need to. addopts = "--cov=laserfiche_mcp --cov-report=term-missing --cov-fail-under=85" [tool.coverage.run] branch = true source = ["src/laserfiche_mcp"] # Integration tests are auto-skipped in default runs so they would otherwise # count as uncovered code. Exclude them from the coverage source. omit = ["tests/*"] [tool.coverage.report] # Lines that are explicitly defensive or only run under tooling we don't # exercise in unit tests (FastMCP lifespan, main() argv parsing). exclude_lines = [ "pragma: no cover", "raise NotImplementedError", "if __name__ == .__main__.:", "if TYPE_CHECKING:", ] show_missing = true skip_covered = false