Source code for novelai_image_mcp.settings

"""Environment-driven configuration for the NovelAI Image MCP server.

Two ``pydantic-settings`` models expose the runtime configuration surface:
``NovelAISettings`` (credentials + generation defaults, ``NOVELAI_*`` env
vars) and ``MCPServerSettings`` (transport, ``MCP_*`` env vars).

``NovelAISettings`` structurally satisfies ``nai.NovelAIConfigLike`` so the
client factory consumes it directly.
"""

from __future__ import annotations

from typing import Literal

from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict


[docs] class NovelAISettings(BaseSettings): """NovelAI credentials, endpoints, and generation defaults. All fields are read from ``NOVELAI_*`` environment variables (and an optional ``.env`` file in the working directory). Exactly one auth method is required at runtime: either ``token`` (preferred) or the ``username``+``password`` pair. """ model_config = SettingsConfigDict( env_prefix="NOVELAI_", env_file=".env", env_file_encoding="utf-8", extra="ignore", case_sensitive=False, ) # ── Credentials ── token: str | None = None username: str | None = None password: str | None = None # ── Endpoints ── # ``image.novelai.net`` is the consolidated host for most third-party API # traffic (generation, director, account, upscaling, ...). # ``legacy_image_base_url`` points at the Primary API # (``api.novelai.net``), which is still the home of # ``/ai/annotate-image`` — the one endpoint that was never migrated and # 404s on ``image.novelai.net``. ``/ai/upscale`` *did* move: it is served # by ``image_base_url`` and accepts only the V5 line. The Primary API's own # docs (https://api.novelai.net/docs/) state that third-party users may use # its ``/ai/`` routes. image_base_url: str = "https://image.novelai.net" account_base_url: str = "https://image.novelai.net" legacy_image_base_url: str = "https://api.novelai.net" timeout: float = Field(default=120.0, gt=0) # ── Generation defaults (overridable per tool call) ── # Default stays on V4.5 for compatibility — V5 is ~2.5x heavier and its # Opus free quota is refillable/limited; switch to "nai-diffusion-5-full" # via NOVELAI_DEFAULT_MODEL when V5 is desired. default_model: str = "nai-diffusion-4-5-full" default_width: int = Field(default=832, ge=64, le=49_152) default_height: int = Field(default=1216, ge=64, le=49_152) default_steps: int = Field(default=28, ge=1, le=50) default_scale: float = Field(default=5.0, gt=0, le=20) default_sampler: str = "k_euler_ancestral" # ── Upscale ── # The standalone upscaler (4x) only accepts the V5 line; this is a `Model` # id. See `nai.constants.supports_upscale`. upscale_model: str = "nai-diffusion-5-full" # ── Client ── vibe_cache_entries: int = Field(default=64, ge=1, le=1024) # ── Output ── output_dir: str = "outputs"
[docs] def has_credentials(self) -> bool: """True when either a token or a complete username/password pair is set.""" return bool(self.token) or bool(self.username and self.password)
[docs] class MCPServerSettings(BaseSettings): """MCP server transport, auth, routing and logging (``MCP_*`` env vars). List- and dict-valued fields are read from the environment as JSON (``pydantic-settings`` decodes complex types with ``json.loads``): ``MCP_MODEL_HEADERS='["X-MCP-Model"]'`` works, while a comma-separated ``X-MCP-Model,X-MCP-Model-Name`` raises a settings error, and an empty string is not a valid value for a ``dict`` field. """ model_config = SettingsConfigDict( env_prefix="MCP_", env_file=".env", env_file_encoding="utf-8", extra="ignore", case_sensitive=False, ) # ``streamable-http`` is the historical alias for the canonical # ``http`` transport; both are accepted so existing scripts keep # working, and ``resolved_transport`` normalises the value passed to # FastMCP. transport: Literal["stdio", "http", "streamable-http"] = "stdio" host: str = "127.0.0.1" port: int = Field(default=8000, ge=1, le=65_535) path: str = "/mcp" # Opt-in bearer token: when set, the HTTP transport requires the # ``Authorization: Bearer <token>`` header. stdio is unaffected. auth_token: str | None = None log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None # ── Insiders opt-in surface ── # The opt-in header is configurable so a deployment can move it out of a # namespace it does not own. Lookup is case-insensitive because HTTP header # names are. insiders_header: str = "X-MCP-Insiders" # Ordered routing candidates: the first header carrying a non-empty value # supplies the corresponding field. JSON list in the environment, e.g. # ``MCP_MODEL_HEADERS='["X-MCP-Model","X-MCP-Model-Name"]'``. model_headers: list[str] = Field( default_factory=lambda: ["X-MCP-Model", "X-MCP-Model-Name"], ) agent_headers: list[str] = Field( default_factory=lambda: [ "X-MCP-Agent-ID", "X-MCP-Agent", "X-MCP-Agent-Source", ], ) client_headers: list[str] = Field( default_factory=lambda: ["X-MCP-Client", "X-MCP-Client-Session-Id"], ) # Operator-level kill switch: a feature listed here is off no matter what a # client sends in the insiders header. JSON list, e.g. # ``MCP_INSIDERS_DISABLED_FEATURES='["model-routing"]'``. insiders_disabled_features: list[str] = Field(default_factory=list) # Audit trail destination; ``None`` means ``<output_dir>/audit.jsonl``. audit_log_path: str | None = None # When true, generated images are written under a per-route subdirectory. route_scoped_output: bool = True # Optional ``{route: token}`` map for route-bound authentication: the key is # the route the token authenticates as. JSON object, e.g. # ``MCP_ROUTE_TOKENS='{"team-a":"s3cret"}'``. route_tokens: dict[str, str] | None = None
[docs] @property def resolved_transport(self) -> Literal["stdio", "http"]: """Canonical transport name (the legacy alias maps to ``http``).""" return "stdio" if self.transport == "stdio" else "http"
[docs] def get_novelai_settings() -> NovelAISettings: """Load NovelAI settings from environment / .env.""" return NovelAISettings()
[docs] def get_mcp_settings() -> MCPServerSettings: """Load MCP server settings from environment / .env.""" return MCPServerSettings()
__all__ = [ "MCPServerSettings", "NovelAISettings", "get_mcp_settings", "get_novelai_settings", ]