[project] name = "image-generation-mcp" version = "1.12.1" description = "MCP server for AI image generation via OpenAI, Google GenAI, or Stable Diffusion WebUI" readme = "README.md" license = "MIT" # project-owned — keep across copier updates requires-python = ">=3.11" authors = [{ name = "Peter van Liesdonk" }] keywords = ["mcp", "image-generation-mcp", "model-context-protocol", "fastmcp"] classifiers = [ "Development Status :: 3 - Alpha", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "License :: OSI Approved :: MIT License", # project-owned — keep across copier updates ] dependencies = [ # >=4.11.3 for ServerConfig.oidc_advertised_scopes, which # config-presentation.yml names: the config-surface generator rejects a # presentation entry for a var core does not declare, so generation fails # outright wherever an older core is resolved. The field is the operator # override for the scopes pvl-core advertises to MCP clients in # protected-resource metadata — `openid offline_access` by default # (pvl-core #280). Before it, the advertised set was derived from # oidc_required_scopes, which multi mode requires to be empty, so clients # requested no scopes at all, the grant carried no refresh token, and # every session ended at access-token expiry needing a human at a browser. # # 4.11.2 was the previous floor, for build_instructions without the # read_only keyword: 4.11.1 declares that argument required and 4.11.2 # rejects it, so the floor and the server.py call site had to move in one # change (#443). 4.11.2 also makes FetchResult.final_url mandatory, so a # consumer constructing one — a project's test fakes included — must pass # it from that floor on. # # 4.11.0 was the floor before that, for configure_task_backend + # ServerConfig.tasks_url (the background-task backend surface, ADR 0002 # §4), which server.py wires unconditionally. 4.11.0 also moves # fastmcp[tasks] into pvl-core's BASE dependencies, so pydocket arrives # transitively — no extra to declare here, and no packaging plumbing. # 4.10.1 was the floor for apply_tool_visibility, named in place of the # 4.10.0 that was tagged but never reached PyPI: the generator resolves # this whole constraint rather than the floor alone (#335), but a floor # naming a release nobody can install is still a trap for anyone pinning # to it. "fastmcp-pvl-core>=4.11.3,<5", "typer>=0.12", # PROJECT-DEPS-START — add domain dependencies below; kept across copier update "Pillow>=10.0", # PROJECT-DEPS-END ] [project.optional-dependencies] # PROJECT-EXTRAS-START — add domain-specific optional-dependency groups below; kept across copier update # fastmcp-pvl-core already pins fastmcp>=3,<4; adding `[tasks]` here # attaches the task-queue extra on top. mcp = ["fastmcp[tasks]>=3.0,<4"] openai = ["openai>=1.0"] google-genai = ["google-genai>=1.0"] all = ["fastmcp[tasks]>=3.0,<4", "openai>=1.0", "google-genai>=1.0"] # Pulls debugpy transitively via fastmcp-pvl-core's [debug] extra. The # version range is inherited from the [project] dependency on # fastmcp-pvl-core above; pip resolves a single, consistent version. debug = ["fastmcp-pvl-core[debug]"] # PROJECT-EXTRAS-END # Tooling lives under PEP 735 [dependency-groups] (not [project.optional-dependencies]) # so it never ships to PyPI consumers and never lands in the security-audit input. [dependency-groups] dev = [ "image-generation-mcp[all]", "pytest>=7.0", "pytest-asyncio>=0.21", "pytest-cov>=4.0", "diff-cover>=9.0", "ruff>=0.6", "mypy>=1.0", "pip-audit>=2.7", "jsonschema>=4.18", # scripts/gen_config_surface.py imports yaml. It arrived transitively via # mkdocs-material in the `docs` group, so a plain `uv sync` left it out # and the generator fell back to its network bootstrap — slower, offline- # hostile, and liable to resolve a different fastmcp-pvl-core than this # project pins (#262). Declared here so the requirement is stated. "pyyaml>=6", ] docs = [ "mike>=2", "mkdocs-material>=9.7.6", "mkdocstrings[python]>=0.28", "mkdocs-llmstxt>=0.5", ] browser = [ "pytest-playwright>=0.5", ] [project.scripts] image-generation-mcp = "image_generation_mcp.cli:main" [project.urls] Repository = "https://github.com/pvliesdonk/image-generation-mcp" Documentation = "https://pvliesdonk.github.io/image-generation-mcp/" Issues = "https://github.com/pvliesdonk/image-generation-mcp/issues" [build-system] # Bounded on purpose, at the minor. hatchling emits the newest core-metadata # version it knows, and the publisher's twine refuses metadata it does not # know -- an invariant spanning this file and release.yml's # gh-action-pypi-publish pin. Unpinned, this side moves on hatchling's # release schedule rather than on anyone's decision, and the break lands at # release time in every project at once (#479). # # The ceiling has to bind at the minor, because that is where the metadata # version moves: 1.27 took it to 2.4 and 1.32 took it to 2.5. A `<2` ceiling # would have stopped neither, so it buys the look of a guard without one. # Patch releases stay open; a minor bump is a deliberate edit here, paired # with a publisher that accepts whatever the new minor emits. requires = ["hatchling>=1.32,<1.33"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["src/image_generation_mcp"] [tool.ruff] target-version = "py311" line-length = 88 src = ["src", "tests"] [tool.ruff.lint] select = [ "E", "W", "F", "I", "B", "C4", "UP", "ARG", "SIM", "TC", "PTH", "RUF", ] ignore = ["E501"] [tool.ruff.lint.per-file-ignores] # Depends() and CurrentContext() calls in argument defaults are the standard # FastMCP dependency injection pattern — B008 is a false positive here. # Context/FastMCP/Service imports must stay runtime (not TYPE_CHECKING) for # DI resolution — TC001/TC002/TC003. # Template-managed: the structural diff-gate adds `S` (security) on the diff # pass, so the shipped structural-gate test would self-block: its asserts trip # S101, and its bare-name `git` call trips S607 (a partial executable path — # the `sys.executable -m ...` calls are NOT flagged). S603 (subprocess use) is # included defensively: inert in current ruff but emitted by some versions. # Ignoring all three in tests keeps the gate from self-blocking on its own test # module; all three are safe in test code. No-op for the base whole-repo select # (which excludes S). If your project needs its own tests/** ignores, EXTEND # this list in place — do not add a second "tests/**" key (duplicate TOML key). "tests/**" = ["S101", "S603", "S607"] # The config-surface generator's bootstrap re-execs itself under `uv run` # (os.execvpe with a `shutil.which`-resolved absolute path — see # ensure_core_available). S606 flags any no-shell process start for review; # here the no-shell form is the safer one, and a `# noqa` instead of this # entry would trip RUF100 on the whole-repo pass, where `S` isn't selected. "scripts/gen_config_surface.py" = ["S606"] # The release stamper stages its rewrites with a fixed-argv `git add` # (subprocess with a list, never a shell). S607 flags the bare `git` name # (a partial executable path) and S603 the subprocess call itself; both are # safe here and would otherwise trip every adopter's structural diff-gate # the first time an update touches the template-owned parts of the script. "scripts/stamp_manifests.py" = ["S603", "S607"] # PROJECT-RUFF-IGNORES-START — replace, extend, or override the entries below # for your project's real source/test paths (split _server_apps.py into # per-domain modules, rename tests, add per-file overrides as needed); kept # across copier update. # PLR0913 (too-many-args) is ignored on the DI/wiring modules below: FastMCP # inflates their arg counts with `Context` + `Depends()` injections, so the # structural diff-gate would flag every new tool/resource/prompt and make # `# noqa: PLR0913` routine. It only bites when enable_structural_gate is on # (PLR0913 isn't in the base select); domain modules keep the check, and # C901/PLR0912/PLR0915 still catch a genuinely complex tool here. # tools.py additionally ignores C901/PLR0915: register_tools is a flat # registration hub (one @mcp.tool block per tool) whose statement/cyclomatic # count scales with the tool list, not with real complexity — PLR0912 # (branches) stays active to catch a genuinely complex individual tool. "src/image_generation_mcp/server.py" = ["B008", "TC001", "TC002", "PLR0913"] "src/image_generation_mcp/cli.py" = ["B008", "PLR0913"] "src/image_generation_mcp/_server_deps.py" = ["B008", "TC002", "TC003", "PLR0913"] "src/image_generation_mcp/tools.py" = ["B008", "TC001", "TC002", "PLR0913", "C901", "PLR0915"] "src/image_generation_mcp/resources.py" = ["B008", "TC001", "TC002", "PLR0913"] "src/image_generation_mcp/prompts.py" = ["B008", "TC002", "PLR0913"] # Path is used at runtime in ImageRecord dataclass. "src/image_generation_mcp/domain.py" = ["TC003"] "src/image_generation_mcp/providers/*.py" = ["TC002"] # pytest fixtures need runtime imports; TC003 also broadly suppressed to match # the template's intent (template targets `tests/conftest.py` + `tests/test_smoke.py` # explicitly with TC003 — broadening here pre-empts a future copier conflict). "tests/*.py" = ["TC002", "TC003"] # PROJECT-RUFF-IGNORES-END [tool.ruff.lint.isort] known-first-party = ["image_generation_mcp"] [tool.ruff.format] quote-style = "double" indent-style = "space" # Structural-gate thresholds. These rules are NOT in the whole-repo `select` # above — they apply only on the diff via the structural gate's # `--extend-select` (see .pre-commit-config.yaml / ci.yml). Values are ruff # defaults, surfaced here as editable policy constants. [tool.ruff.lint.mccabe] max-complexity = 10 [tool.ruff.lint.pylint] max-args = 5 max-branches = 12 max-returns = 6 max-statements = 50 [tool.pytest.ini_options] testpaths = ["tests"] addopts = ["--strict-markers", "-ra"] asyncio_mode = "auto" markers = [ "browser: end-to-end test that drives a headless browser (requires the 'browser' dependency group + 'playwright install chromium')", ] [tool.coverage.run] source = ["src/image_generation_mcp"] branch = true [tool.coverage.report] exclude_lines = [ "pragma: no cover", "def __repr__", "raise AssertionError", "raise NotImplementedError", "if TYPE_CHECKING:", "if __name__ == .__main__.:", ] [tool.mypy] python_version = "3.11" strict = true warn_return_any = true warn_unused_ignores = true disallow_untyped_defs = true disallow_incomplete_defs = true check_untyped_defs = true no_implicit_optional = true [[tool.mypy.overrides]] module = ["fastmcp", "fastmcp.*"] ignore_missing_imports = true [[tool.mypy.overrides]] module = ["google", "google.genai", "google.genai.*"] ignore_missing_imports = true [[tool.mypy.overrides]] module = ["playwright", "playwright.*"] ignore_missing_imports = true [[tool.mypy.overrides]] module = ["jsonschema", "jsonschema.*"] ignore_missing_imports = true # Tests use FastMCP private surfaces (e.g. ``tool.fn``) and pytest fixtures # that mypy --strict cannot type without invasive annotation. Loosen the # strict knobs for the test tree so the ``mypy src/ tests/`` gate from # template v1.2.0 stays useful (still catches real bugs) without burying us # in 290 false-positive errors. Tightening this is tracked as follow-up # work — see the README's Local development section. [[tool.mypy.overrides]] module = ["tests.*"] disallow_untyped_defs = false disallow_incomplete_defs = false warn_return_any = false warn_unused_ignores = false # stale `type: ignore` comments from earlier configs disable_error_code = [ "attr-defined", # fastmcp Tool.fn private surface used by tests "union-attr", # mcp.get_tool() returns Tool | None; tests assert non-None "no-untyped-def", # pytest fixtures intentionally untyped "no-untyped-call", # importlib-loaded helpers (test_render_model_catalog) are untyped "type-arg", # bare list/tuple/dict in test helpers "var-annotated", # local helper vars without annotations "misc", # async-generator return-type quirks "arg-type", # narrow dict[str, X] vs dict[str, object] in helpers "assignment", # tuple-narrowing in fixture rebinding ] [tool.pyright] # Point Pyright at the uv-managed venv so editor diagnostics resolve # imports the same way as our CLI tools (mypy/ruff/pytest run via uv). venvPath = "." venv = ".venv" pythonVersion = "3.11" # src-layout editable installs use a .pth shim that Pyright does not execute; # point Pyright at src/ directly so IDE import resolution matches runtime. extraPaths = ["src"]