Source code for novelai_image_mcp.nai.service
"""High-level NovelAI service construction for the MCP server and CLI consumers.
A duck-typed config object (``NovelAIConfigLike`` Protocol) keeps the ``nai``
package decoupled from the MCP wrapper. The wrapper's ``NovelAISettings``
(pydantic-settings, env-driven) satisfies this protocol structurally; see
``novelai_image_mcp.settings``.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Protocol, runtime_checkable
from .auth import NovelAICredentials
from .client import MissingNovelAITokenError, NovelAIClient
if TYPE_CHECKING:
import httpx
[docs]
@runtime_checkable
class NovelAIConfigLike(Protocol):
"""Structural shape of any object supplying NovelAI client configuration."""
token: str | None
username: str | None
password: str | None
image_base_url: str
account_base_url: str
timeout: float
vibe_cache_entries: int
[docs]
def create_novelai_client(
config: NovelAIConfigLike,
*,
http_client: httpx.AsyncClient | None = None,
) -> NovelAIClient:
"""Create a complete client from a configuration object.
Accepts either a token or a complete username/password pair. Pass a shared
``http_client`` when the caller (e.g. the MCP lifespan) wants connection
pooling across requests; otherwise the client owns a private session.
"""
if not config.token and not (config.username and config.password):
raise MissingNovelAITokenError("NovelAI credentials are not configured")
return NovelAIClient(
NovelAICredentials(
token=config.token,
username=config.username,
password=config.password,
),
http_client=http_client,
image_base_url=config.image_base_url,
account_base_url=config.account_base_url,
timeout=config.timeout,
vibe_cache_entries=config.vibe_cache_entries,
)
__all__ = ["NovelAIConfigLike", "create_novelai_client"]