novelai_image_mcp.nai — NovelAI HTTP client

The nai/ subpackage is the NovelAI HTTP client. It is MCP-agnostic — it can be used standalone from any async Python code, independent of the MCP server.

Submodules

novelai_image_mcp.nai.client

The high-level async client. Wraps the wire-format encoder (payload.build_generation_payload) and response decoder (response.parse_messagepack_images) over a shared httpx.AsyncClient.

Complete NovelAI HTTP client built on a shared httpx async session.

A long-lived httpx.AsyncClient is owned by the instance (or supplied by the MCP lifespan) and used for all NovelAI API traffic. Covers generation, Director, utility, and account business logic.

exception novelai_image_mcp.nai.client.MissingNovelAITokenError(message, *, code=None, explanation=None)[source]

Bases: NovelAIAuthenticationError

No usable NovelAI credential is configured.

Parameters:
  • message (str)

  • code (int | str | None)

  • explanation (str | None)

Return type:

None

class novelai_image_mcp.nai.client.NovelAIClient(credentials, *, http_client=None, image_base_url='https://image.novelai.net', account_base_url='https://image.novelai.net', legacy_image_base_url='https://api.novelai.net', timeout=120.0, vibe_cache_entries=64)[source]

Bases: object

Project-owned client for generation, tools, utilities, and account APIs.

Parameters:
  • credentials (NovelAICredentials)

  • http_client (httpx.AsyncClient | None)

  • image_base_url (str)

  • account_base_url (str)

  • legacy_image_base_url (str)

  • timeout (float)

  • vibe_cache_entries (int)

async aclose()[source]

Close the underlying HTTP session when owned by this client.

Return type:

None

async get_access_token()[source]
Return type:

str

async encode_vibe(reference, *, information_extracted, model)[source]
Parameters:
Return type:

str

async generate(request)[source]
Parameters:

request (GenerationRequest)

Return type:

tuple[NovelAIImage, …]

async stream_generation(request)[source]

Yield V4 / V4.5 / V5 events as the active driver produces chunks.

Parameters:

request (GenerationRequest)

Return type:

AsyncIterator[GenerationEvent]

async director(tool, image, *, prompt='', defry=0, emotion=None, emotion_level=EmotionLevel.NORMAL)[source]
Parameters:
Return type:

NovelAIImage

async upscale(image, *, factor=4)[source]
Parameters:
Return type:

NovelAIImage

async annotate(image, model)[source]
Parameters:
Return type:

NovelAIImage

async suggest_tags(prompt, *, model=Model.V4_5, language='en')[source]
Parameters:
Return type:

tuple[dict[str, Any], …]

async get_subscription()[source]
Return type:

dict[str, Any]

async get_user_data()[source]
Return type:

dict[str, Any]

exception novelai_image_mcp.nai.client.NovelAIError(message, *, code=None, explanation=None)[source]

Bases: Exception

Base class for NovelAI failures.

code is the NovelAI error code when known (None for transport, timeout, or local validation failures that carry no server code); explanation is NovelAI’s official explanation for documented codes.

Parameters:
  • message (str)

  • code (int | str | None)

  • explanation (str | None)

Return type:

None

exception novelai_image_mcp.nai.client.NovelAIProviderError(message, *, code=None, explanation=None)[source]

Bases: NovelAIError

NovelAI rejected or failed the request.

Parameters:
  • message (str)

  • code (int | str | None)

  • explanation (str | None)

Return type:

None

exception novelai_image_mcp.nai.client.NovelAIResponseError(message, *, code=None, explanation=None)[source]

Bases: NovelAIError

NovelAI returned malformed or unsupported data.

Parameters:
  • message (str)

  • code (int | str | None)

  • explanation (str | None)

Return type:

None

exception novelai_image_mcp.nai.client.NovelAITimeoutError(message, *, code=None, explanation=None)[source]

Bases: NovelAIError

A request exceeded its timeout.

Parameters:
  • message (str)

  • code (int | str | None)

  • explanation (str | None)

Return type:

None

exception novelai_image_mcp.nai.client.NovelAITransportError(message, *, code=None, explanation=None)[source]

Bases: NovelAIError

The request could not reach NovelAI.

Parameters:
  • message (str)

  • code (int | str | None)

  • explanation (str | None)

Return type:

None

novelai_image_mcp.nai.client.extract_final_image(content)[source]
Parameters:

content (bytes)

Return type:

bytes

async novelai_image_mcp.nai.client.generate_image_from_plan(plan, *, client, n_samples=1, quality=True, uc_preset=0, noise_schedule='karras', cfg_rescale=0.0, dynamic_thresholding=False, auto_smea=False, prefer_brownian=True)[source]

Generate a single image from a high-level plan (used by the sync CLI).

The caller supplies the configured client plus the generation defaults (read from settings, not from a global config).

Parameters:
Return type:

bytes

novelai_image_mcp.nai.constants

Enums for the NovelAI API surface: Action, Model, Sampler, DirectorTool, Emotion, EmotionLevel, ControlNetModel, Endpoint, NoiseSchedule. Plus the predicates is_v4_model, is_v5_model, supports_vibe, and is_inpaint_model.

NovelAI protocol constants and model capability helpers.

class novelai_image_mcp.nai.constants.IntEnum(new_class_name, /, names, *, module=None, qualname=None, type=None, start=1, boundary=None)[source]

Bases: int, ReprEnum

Enum where members are also (and must be) ints

class novelai_image_mcp.nai.constants.StrEnum(new_class_name, /, names, *, module=None, qualname=None, type=None, start=1, boundary=None)[source]

Bases: str, ReprEnum

Enum where members are also (and must be) strings

__new_member__(*values)

values must already be of type str

class novelai_image_mcp.nai.constants.Endpoint(*values)[source]

Bases: StrEnum

NovelAI API path constants.

Most paths are rooted at https://image.novelai.net (the consolidated third-party host). Two exceptions — UPSCALE and ANNOTATE — were not migrated to image.novelai.net and remain on the Primary API at https://api.novelai.net. The Primary API’s docs (https://api.novelai.net/docs/) state that third-party users may use its /ai/ routes. See NovelAIClient.legacy_image_base_url.

The /ai/generate-image family is split by model generation:

  • IMAGE (/ai/generate-image) — used by V3 / Furry legacy models. Returns a ZIP archive (application/zip) with HTTP 201.

  • IMAGE_STREAM (/ai/generate-image-stream) — used by V4 / V4.5 / V5 new models. Returns a MessagePack stream (stream: "msgpack") with HTTP 200. The selection is driven by is_v4_model in client.py.

LOGIN = '/user/login'
USER_DATA = '/user/data'
SUBSCRIPTION = '/user/subscription'
IMAGE = '/ai/generate-image'
IMAGE_STREAM = '/ai/generate-image-stream'
DIRECTOR = '/ai/augment-image'
ENCODE_VIBE = '/ai/encode-vibe'
UPSCALE = '/ai/upscale'
ANNOTATE = '/ai/annotate-image'
SUGGEST_TAGS = '/ai/generate-image/suggest-tags'
class novelai_image_mcp.nai.constants.Model(*values)[source]

Bases: StrEnum

V3 = 'nai-diffusion-3'
V3_INPAINT = 'nai-diffusion-3-inpainting'
FURRY = 'nai-diffusion-furry-3'
FURRY_INPAINT = 'nai-diffusion-furry-3-inpainting'
V4 = 'nai-diffusion-4-full'
V4_INPAINT = 'nai-diffusion-4-full-inpainting'
V4_CURATED = 'nai-diffusion-4-curated-preview'
V4_CURATED_INPAINT = 'nai-diffusion-4-curated-inpainting'
V4_5 = 'nai-diffusion-4-5-full'
V4_5_INPAINT = 'nai-diffusion-4-5-full-inpainting'
V4_5_CURATED = 'nai-diffusion-4-5-curated'
V4_5_CURATED_INPAINT = 'nai-diffusion-4-5-curated-inpainting'
V5 = 'nai-diffusion-5-full'
V5_CURATED = 'nai-diffusion-5-curated'
V5_INPAINT = 'nai-diffusion-5-full-inpainting'
class novelai_image_mcp.nai.constants.Action(*values)[source]

Bases: StrEnum

GENERATE = 'generate'
IMG2IMG = 'img2img'
INPAINT = 'infill'
class novelai_image_mcp.nai.constants.Sampler(*values)[source]

Bases: StrEnum

EULER = 'k_euler'
EULER_ANCESTRAL = 'k_euler_ancestral'
DPM_2S_ANCESTRAL = 'k_dpmpp_2s_ancestral'
DPM_2M = 'k_dpmpp_2m'
DPM_2M_SDE = 'k_dpmpp_2m_sde'
DPM_SDE = 'k_dpmpp_sde'
DDIM = 'ddim_v3'
class novelai_image_mcp.nai.constants.NoiseSchedule(*values)[source]

Bases: StrEnum

NATIVE = 'native'
KARRAS = 'karras'
EXPONENTIAL = 'exponential'
POLYEXPONENTIAL = 'polyexponential'
class novelai_image_mcp.nai.constants.ControlNetModel(*values)[source]

Bases: StrEnum

PALETTE_SWAP = 'hed'
FORM_LOCK = 'midas'
SCRIBBLER = 'fake_scribble'
BUILDING_CONTROL = 'mlsd'
LANDSCAPER = 'uniformer'
class novelai_image_mcp.nai.constants.DirectorTool(*values)[source]

Bases: StrEnum

LINE_ART = 'lineart'
SKETCH = 'sketch'
BACKGROUND_REMOVAL = 'bg-removal'
DECLUTTER = 'declutter'
COLORIZE = 'colorize'
EMOTION = 'emotion'
class novelai_image_mcp.nai.constants.Emotion(*values)[source]

Bases: StrEnum

NEUTRAL = 'neutral'
HAPPY = 'happy'
SAD = 'sad'
ANGRY = 'angry'
SCARED = 'scared'
SURPRISED = 'surprised'
TIRED = 'tired'
EXCITED = 'excited'
NERVOUS = 'nervous'
THINKING = 'thinking'
CONFUSED = 'confused'
SHY = 'shy'
DISGUSTED = 'disgusted'
SMUG = 'smug'
BORED = 'bored'
LAUGHING = 'laughing'
IRRITATED = 'irritated'
AROUSED = 'aroused'
EMBARRASSED = 'embarrassed'
WORRIED = 'worried'
LOVE = 'love'
DETERMINED = 'determined'
HURT = 'hurt'
PLAYFUL = 'playful'
class novelai_image_mcp.nai.constants.EmotionLevel(*values)[source]

Bases: IntEnum

NORMAL = 0
SLIGHTLY_WEAK = 1
WEAK = 2
EVEN_WEAKER = 3
VERY_WEAK = 4
WEAKEST = 5
novelai_image_mcp.nai.constants.is_v4_model(model)[source]

True for structured-prompt (V4+) models: V4 / V4.5 / V5 families.

These use the /ai/generate-image-stream endpoint (MessagePack) and the v4_prompt / v4_negative_prompt caption payload shape. The name is retained for backward compatibility (public nai API).

Parameters:

model (Model)

Return type:

bool

novelai_image_mcp.nai.constants.is_v5_model(model)[source]

True for NovelAI Diffusion V5 models (Full / Curated / Inpainting).

Parameters:

model (Model)

Return type:

bool

novelai_image_mcp.nai.constants.supports_vibe(model)[source]

True when vibe transfer (reference images) works for this model.

Vibe transfer is available on V4 / V4.5 only — V3 / Furry and V5 (feature not yet released for V5 per the official announcement) return False.

Parameters:

model (Model)

Return type:

bool

novelai_image_mcp.nai.constants.is_inpaint_model(model)[source]
Parameters:

model (Model)

Return type:

bool

novelai_image_mcp.nai.constants.params_version_for(model)[source]

Return the NovelAI params_version for a model.

V5 clients ship both 3 (Marinara-Engine) and 4 (Auto-NovelAI, SDlab) and the API tolerates either; this project sends 4 for V5 (the value used by the most recent third-party implementations) and 3 for older generations, pending live verification.

Parameters:

model (Model)

Return type:

int

novelai_image_mcp.nai.models

Pydantic models for the wire format: GenerationRequest, CharacterPrompt, NovelAIGenerationPlan.

Pure value objects shared by the NovelAI generation pipeline.

novelai_image_mcp.nai.models.dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False, weakref_slot=False)[source]

Add dunder methods based on the fields defined in the class.

Examines PEP 526 __annotations__ to determine fields.

If init is true, an __init__() method is added to the class. If repr is true, a __repr__() method is added. If order is true, rich comparison dunder methods are added. If unsafe_hash is true, a __hash__() method is added. If frozen is true, fields may not be assigned to after instance creation. If match_args is true, the __match_args__ tuple is added. If kw_only is true, then by default all fields are keyword-only. If slots is true, a new class with a __slots__ attribute is returned.

class novelai_image_mcp.nai.models.Action(*values)[source]

Bases: StrEnum

GENERATE = 'generate'
IMG2IMG = 'img2img'
INPAINT = 'infill'
class novelai_image_mcp.nai.models.Model(*values)[source]

Bases: StrEnum

V3 = 'nai-diffusion-3'
V3_INPAINT = 'nai-diffusion-3-inpainting'
FURRY = 'nai-diffusion-furry-3'
FURRY_INPAINT = 'nai-diffusion-furry-3-inpainting'
V4 = 'nai-diffusion-4-full'
V4_INPAINT = 'nai-diffusion-4-full-inpainting'
V4_CURATED = 'nai-diffusion-4-curated-preview'
V4_CURATED_INPAINT = 'nai-diffusion-4-curated-inpainting'
V4_5 = 'nai-diffusion-4-5-full'
V4_5_INPAINT = 'nai-diffusion-4-5-full-inpainting'
V4_5_CURATED = 'nai-diffusion-4-5-curated'
V4_5_CURATED_INPAINT = 'nai-diffusion-4-5-curated-inpainting'
V5 = 'nai-diffusion-5-full'
V5_CURATED = 'nai-diffusion-5-curated'
V5_INPAINT = 'nai-diffusion-5-full-inpainting'
class novelai_image_mcp.nai.models.NoiseSchedule(*values)[source]

Bases: StrEnum

NATIVE = 'native'
KARRAS = 'karras'
EXPONENTIAL = 'exponential'
POLYEXPONENTIAL = 'polyexponential'
class novelai_image_mcp.nai.models.Sampler(*values)[source]

Bases: StrEnum

EULER = 'k_euler'
EULER_ANCESTRAL = 'k_euler_ancestral'
DPM_2S_ANCESTRAL = 'k_dpmpp_2s_ancestral'
DPM_2M = 'k_dpmpp_2m'
DPM_2M_SDE = 'k_dpmpp_2m_sde'
DPM_SDE = 'k_dpmpp_sde'
DDIM = 'ddim_v3'
novelai_image_mcp.nai.models.is_inpaint_model(model)[source]
Parameters:

model (Model)

Return type:

bool

novelai_image_mcp.nai.models.is_v4_model(model)[source]

True for structured-prompt (V4+) models: V4 / V4.5 / V5 families.

These use the /ai/generate-image-stream endpoint (MessagePack) and the v4_prompt / v4_negative_prompt caption payload shape. The name is retained for backward compatibility (public nai API).

Parameters:

model (Model)

Return type:

bool

novelai_image_mcp.nai.models.normalize_tags(values)[source]

Trim tags and remove case-insensitive duplicates while preserving order.

Parameters:

values (tuple[str, ...])

Return type:

tuple[str, …]

novelai_image_mcp.nai.models.normalize_negative_prompt(value)[source]

Split comma/newline negative prompt text into normalized tags.

Parameters:

value (str | None)

Return type:

tuple[str, …]

class novelai_image_mcp.nai.models.PositionCoord(x: 'float', y: 'float')[source]

Bases: object

Parameters:
x: float
y: float
class novelai_image_mcp.nai.models.CharacterIntent(description: 'str', tags: 'tuple[str, ...]', negative_tags: 'tuple[str, ...]', center: 'PositionCoord')[source]

Bases: object

Parameters:
description: str
tags: tuple[str, ...]
negative_tags: tuple[str, ...]
center: PositionCoord
class novelai_image_mcp.nai.models.GenerationHints(width: 'int | None' = None, height: 'int | None' = None, steps: 'int | None' = None, scale: 'float | None' = None, sampler: 'str | None' = None, seed: 'int | None' = None, negative_tags: 'tuple[str, ...]' = ())[source]

Bases: object

Parameters:
  • width (int | None)

  • height (int | None)

  • steps (int | None)

  • scale (float | None)

  • sampler (str | None)

  • seed (int | None)

  • negative_tags (tuple[str, ...])

width: int | None
height: int | None
steps: int | None
scale: float | None
sampler: str | None
seed: int | None
negative_tags: tuple[str, ...]
class novelai_image_mcp.nai.models.PromptIntent(source_language: 'str', english_description: 'str', base_tags: 'tuple[str, ...]', generation: 'GenerationHints', characters: 'tuple[CharacterIntent, ...]', search_required: 'bool', search_query: 'str | None', search_reason: 'str | None')[source]

Bases: object

Parameters:
source_language: str
english_description: str
base_tags: tuple[str, ...]
generation: GenerationHints
characters: tuple[CharacterIntent, ...]
search_required: bool
search_query: str | None
search_reason: str | None
class novelai_image_mcp.nai.models.VisualResearch(facts: 'tuple[str, ...]', sources: 'tuple[str, ...]')[source]

Bases: object

Parameters:
facts: tuple[str, ...]
sources: tuple[str, ...]
class novelai_image_mcp.nai.models.TipoRequest(description: 'str', tags: 'tuple[str, ...]', visual_facts: 'tuple[str, ...]', seed: 'int')[source]

Bases: object

Parameters:
description: str
tags: tuple[str, ...]
visual_facts: tuple[str, ...]
seed: int
class novelai_image_mcp.nai.models.TipoPrompt(natural_language: 'str', tags: 'tuple[str, ...]')[source]

Bases: object

Parameters:
natural_language: str
tags: tuple[str, ...]
class novelai_image_mcp.nai.models.GenerationOverrides(width: 'int | None' = None, height: 'int | None' = None, steps: 'int | None' = None, scale: 'float | None' = None, sampler: 'str | None' = None, seed: 'int | None' = None, negative_prompt: 'str | None' = None)[source]

Bases: object

Parameters:
  • width (int | None)

  • height (int | None)

  • steps (int | None)

  • scale (float | None)

  • sampler (str | None)

  • seed (int | None)

  • negative_prompt (str | None)

width: int | None
height: int | None
steps: int | None
scale: float | None
sampler: str | None
seed: int | None
negative_prompt: str | None
class novelai_image_mcp.nai.models.NovelAIGenerationPlan(prompt: 'str', negative_prompt: 'str', width: 'int', height: 'int', steps: 'int', scale: 'float', sampler: 'str', seed: 'int', base_caption: 'str', char_captions: 'tuple[dict[str, object], ...]', character_prompts: 'tuple[dict[str, object], ...]', use_coords: 'bool')[source]

Bases: object

Parameters:
prompt: str
negative_prompt: str
width: int
height: int
steps: int
scale: float
sampler: str
seed: int
base_caption: str
char_captions: tuple[dict[str, object], ...]
character_prompts: tuple[dict[str, object], ...]
use_coords: bool
class novelai_image_mcp.nai.models.CharacterPrompt(prompt: 'str', negative_prompt: 'str' = '', x: 'float' = 0.5, y: 'float' = 0.5, enabled: 'bool' = True)[source]

Bases: object

Parameters:
prompt: str
negative_prompt: str
x: float
y: float
enabled: bool
class novelai_image_mcp.nai.models.GenerationRequest(prompt, base_caption=None, model=Model.V4_5, action=Action.GENERATE, negative_prompt='', width=832, height=1216, n_samples=1, steps=28, scale=5.0, sampler=Sampler.EULER_ANCESTRAL, seed=0, extra_noise_seed=None, noise_schedule=NoiseSchedule.KARRAS, quality=True, uc_preset=0, dynamic_thresholding=False, cfg_rescale=0.0, smea=None, smea_dynamic=None, auto_smea=False, image=None, mask=None, strength=None, noise=None, add_original_image=True, controlnet_strength=1.0, controlnet_condition=None, controlnet_model=None, references=(), reference_information=(), reference_strengths=(), character_prompts=(), use_coords=False, use_order=True, legacy_uc=False, normalize_reference_strengths=True, deliberate_euler_ancestral_bug=False, prefer_brownian=True, skip_cfg_above_sigma=None, legacy=False, legacy_v3_extend=False, inpaint_img2img_strength=None, straight_alpha=False)[source]

Bases: object

Complete NovelAI generation request independent of the chat planner.

Parameters:
prompt: str
base_caption: str | None
model: Model
action: Action
negative_prompt: str
width: int
height: int
n_samples: int
steps: int
scale: float
sampler: Sampler | str
seed: int
extra_noise_seed: int | None
noise_schedule: NoiseSchedule | str
quality: bool
uc_preset: int
dynamic_thresholding: bool
cfg_rescale: float
smea: bool | None
smea_dynamic: bool | None
auto_smea: bool
image: str | None
mask: str | None
strength: float | None
noise: float | None
add_original_image: bool
controlnet_strength: float
controlnet_condition: str | None
controlnet_model: str | None
references: tuple[str, ...]
reference_information: tuple[float, ...]
reference_strengths: tuple[float, ...]
character_prompts: tuple[CharacterPrompt, ...]
use_coords: bool
use_order: bool
legacy_uc: bool
normalize_reference_strengths: bool
deliberate_euler_ancestral_bug: bool
prefer_brownian: bool
skip_cfg_above_sigma: int | None
legacy: bool
legacy_v3_extend: bool
inpaint_img2img_strength: int | None
straight_alpha: bool
property effective_prompt: str
property effective_base_caption: str
property effective_negative_prompt: str
property max_samples: int
estimate_anlas_cost(*, opus=False)[source]

Estimate provider Anlas cost using the public web-client formula.

Note: on V5, Opus free generations (normal resolution, <=28 steps) are subject to NovelAI’s refillable “battery” usage limit; once exhausted the API silently bills Anlas instead of erroring, so the estimate here covers the paid cost regardless of quota state.

Parameters:

opus (bool)

Return type:

int

novelai_image_mcp.nai.auth

Argon2id access-key derivation + per-request tracking headers.

NovelAI credential validation and access-key derivation.

novelai_image_mcp.nai.auth.dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False, weakref_slot=False)[source]

Add dunder methods based on the fields defined in the class.

Examines PEP 526 __annotations__ to determine fields.

If init is true, an __init__() method is added to the class. If repr is true, a __repr__() method is added. If order is true, rich comparison dunder methods are added. If unsafe_hash is true, a __hash__() method is added. If frozen is true, fields may not be assigned to after instance creation. If match_args is true, the __match_args__ tuple is added. If kw_only is true, then by default all fields are keyword-only. If slots is true, a new class with a __slots__ attribute is returned.

class novelai_image_mcp.nai.auth.datetime(year, month, day[, hour[, minute[, second[, microsecond[, tzinfo]]]]])

Bases: date

The year, month and day arguments are required. tzinfo may be None, or an instance of a tzinfo subclass. The remaining arguments may be ints.

astimezone()

tz -> convert to local time in new timezone tz

classmethod combine()

date, time -> datetime with same date and time fields

ctime()

Return ctime() style string.

date()

Return date object with same year, month and day.

dst()

Return self.tzinfo.dst(self).

fold
classmethod fromisoformat(object, /)

string -> datetime from a string in most ISO 8601 formats

classmethod fromtimestamp()

timestamp[, tz] -> tz’s local time from POSIX timestamp.

hour
isoformat()

[sep] -> string in ISO 8601 format, YYYY-MM-DDT[HH[:MM[:SS[.mmm[uuu]]]]][+HH:MM]. sep is used to separate the year from the time, and defaults to ‘T’. The optional argument timespec specifies the number of additional terms of the time to include. Valid options are ‘auto’, ‘hours’, ‘minutes’, ‘seconds’, ‘milliseconds’ and ‘microseconds’.

max = datetime.datetime(9999, 12, 31, 23, 59, 59, 999999)
microsecond
min = datetime.datetime(1, 1, 1, 0, 0)
minute
classmethod now(tz=None)

Returns new datetime object representing current time local to tz.

tz

Timezone object.

If no tz is specified, uses local timezone.

replace()

Return datetime with new specified fields.

resolution = datetime.timedelta(microseconds=1)
second
classmethod strptime()

string, format -> new datetime parsed from a string (like time.strptime()).

time()

Return time object with same time but with tzinfo=None.

timestamp()

Return POSIX timestamp as float.

timetuple()

Return time tuple, compatible with time.localtime().

timetz()

Return time object with same time and tzinfo.

tzinfo
tzname()

Return self.tzinfo.tzname(self).

classmethod utcfromtimestamp()

Construct a naive UTC datetime from a POSIX timestamp.

classmethod utcnow()

Return a new datetime representing UTC day and time.

utcoffset()

Return self.tzinfo.utcoffset(self).

utctimetuple()

Return UTC time tuple, compatible with time.localtime().

class novelai_image_mcp.nai.auth.blake2b(data=b'', *, digest_size=64, key=b'', salt=b'', person=b'', fanout=1, depth=1, leaf_size=0, node_offset=0, node_depth=0, inner_size=0, last_node=False, usedforsecurity=True, string=None)

Bases: object

Return a new BLAKE2b hash object.

MAX_DIGEST_SIZE = 64
MAX_KEY_SIZE = 64
PERSON_SIZE = 16
SALT_SIZE = 16
block_size
copy()

Return a copy of the hash object.

digest()

Return the digest value as a bytes object.

digest_size
hexdigest()

Return the digest value as a string of hexadecimal digits.

name
update(data, /)

Update this hash object’s state with the provided bytes-like object.

class novelai_image_mcp.nai.auth.NovelAICredentials(token: 'str | None' = None, username: 'str | None' = None, password: 'str | None' = None)[source]

Bases: object

Parameters:
  • token (str | None)

  • username (str | None)

  • password (str | None)

token: str | None
username: str | None
password: str | None
novelai_image_mcp.nai.auth.derive_access_key(credentials)[source]

Derive the 64-character NovelAI login key from username/password.

Parameters:

credentials (NovelAICredentials)

Return type:

str

novelai_image_mcp.nai.auth.request_tracking_headers()[source]

Build fresh tracking headers expected by NovelAI’s browser-facing APIs.

Return type:

dict[str, str]

novelai_image_mcp.nai.payload

build_generation_payload — wire-format encoder (MessagePack-compatible).

NovelAI V4+ / V5 wire payload mapping.

class novelai_image_mcp.nai.payload.Model(*values)[source]

Bases: StrEnum

V3 = 'nai-diffusion-3'
V3_INPAINT = 'nai-diffusion-3-inpainting'
FURRY = 'nai-diffusion-furry-3'
FURRY_INPAINT = 'nai-diffusion-furry-3-inpainting'
V4 = 'nai-diffusion-4-full'
V4_INPAINT = 'nai-diffusion-4-full-inpainting'
V4_CURATED = 'nai-diffusion-4-curated-preview'
V4_CURATED_INPAINT = 'nai-diffusion-4-curated-inpainting'
V4_5 = 'nai-diffusion-4-5-full'
V4_5_INPAINT = 'nai-diffusion-4-5-full-inpainting'
V4_5_CURATED = 'nai-diffusion-4-5-curated'
V4_5_CURATED_INPAINT = 'nai-diffusion-4-5-curated-inpainting'
V5 = 'nai-diffusion-5-full'
V5_CURATED = 'nai-diffusion-5-curated'
V5_INPAINT = 'nai-diffusion-5-full-inpainting'
novelai_image_mcp.nai.payload.is_v4_model(model)[source]

True for structured-prompt (V4+) models: V4 / V4.5 / V5 families.

These use the /ai/generate-image-stream endpoint (MessagePack) and the v4_prompt / v4_negative_prompt caption payload shape. The name is retained for backward compatibility (public nai API).

Parameters:

model (Model)

Return type:

bool

novelai_image_mcp.nai.payload.is_v5_model(model)[source]

True for NovelAI Diffusion V5 models (Full / Curated / Inpainting).

Parameters:

model (Model)

Return type:

bool

novelai_image_mcp.nai.payload.params_version_for(model)[source]

Return the NovelAI params_version for a model.

V5 clients ship both 3 (Marinara-Engine) and 4 (Auto-NovelAI, SDlab) and the API tolerates either; this project sends 4 for V5 (the value used by the most recent third-party implementations) and 3 for older generations, pending live verification.

Parameters:

model (Model)

Return type:

int

class novelai_image_mcp.nai.payload.GenerationRequest(prompt, base_caption=None, model=Model.V4_5, action=Action.GENERATE, negative_prompt='', width=832, height=1216, n_samples=1, steps=28, scale=5.0, sampler=Sampler.EULER_ANCESTRAL, seed=0, extra_noise_seed=None, noise_schedule=NoiseSchedule.KARRAS, quality=True, uc_preset=0, dynamic_thresholding=False, cfg_rescale=0.0, smea=None, smea_dynamic=None, auto_smea=False, image=None, mask=None, strength=None, noise=None, add_original_image=True, controlnet_strength=1.0, controlnet_condition=None, controlnet_model=None, references=(), reference_information=(), reference_strengths=(), character_prompts=(), use_coords=False, use_order=True, legacy_uc=False, normalize_reference_strengths=True, deliberate_euler_ancestral_bug=False, prefer_brownian=True, skip_cfg_above_sigma=None, legacy=False, legacy_v3_extend=False, inpaint_img2img_strength=None, straight_alpha=False)[source]

Bases: object

Complete NovelAI generation request independent of the chat planner.

Parameters:
prompt: str
base_caption: str | None
model: Model
action: Action
negative_prompt: str
width: int
height: int
n_samples: int
steps: int
scale: float
sampler: Sampler | str
seed: int
extra_noise_seed: int | None
noise_schedule: NoiseSchedule | str
quality: bool
uc_preset: int
dynamic_thresholding: bool
cfg_rescale: float
smea: bool | None
smea_dynamic: bool | None
auto_smea: bool
image: str | None
mask: str | None
strength: float | None
noise: float | None
add_original_image: bool
controlnet_strength: float
controlnet_condition: str | None
controlnet_model: str | None
references: tuple[str, ...]
reference_information: tuple[float, ...]
reference_strengths: tuple[float, ...]
character_prompts: tuple[CharacterPrompt, ...]
use_coords: bool
use_order: bool
legacy_uc: bool
normalize_reference_strengths: bool
deliberate_euler_ancestral_bug: bool
prefer_brownian: bool
skip_cfg_above_sigma: int | None
legacy: bool
legacy_v3_extend: bool
inpaint_img2img_strength: int | None
straight_alpha: bool
property effective_base_caption: str
property effective_negative_prompt: str
property effective_prompt: str
estimate_anlas_cost(*, opus=False)[source]

Estimate provider Anlas cost using the public web-client formula.

Note: on V5, Opus free generations (normal resolution, <=28 steps) are subject to NovelAI’s refillable “battery” usage limit; once exhausted the API silently bills Anlas instead of erroring, so the estimate here covers the paid cost regardless of quota state.

Parameters:

opus (bool)

Return type:

int

property max_samples: int
class novelai_image_mcp.nai.payload.NovelAIGenerationPlan(prompt: 'str', negative_prompt: 'str', width: 'int', height: 'int', steps: 'int', scale: 'float', sampler: 'str', seed: 'int', base_caption: 'str', char_captions: 'tuple[dict[str, object], ...]', character_prompts: 'tuple[dict[str, object], ...]', use_coords: 'bool')[source]

Bases: object

Parameters:
prompt: str
negative_prompt: str
width: int
height: int
steps: int
scale: float
sampler: str
seed: int
base_caption: str
char_captions: tuple[dict[str, object], ...]
character_prompts: tuple[dict[str, object], ...]
use_coords: bool
novelai_image_mcp.nai.payload.build_payload(plan, *, model)[source]

Map an already-resolved plan to NovelAI’s V4.5/V5 request shape.

Parameters:
Return type:

dict[str, Any]

novelai_image_mcp.nai.payload.build_generation_payload(request)[source]

Serialize every supported generation and conditioning option.

Parameters:

request (GenerationRequest)

Return type:

dict[str, Any]

novelai_image_mcp.nai.response

Response parsing: NovelAIImage, parse_messagepack_images, parse_zip_images, check_status, GenerationEvent.

NovelAI HTTP, ZIP, and MessagePack response parsing.

novelai_image_mcp.nai.response.dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False, weakref_slot=False)[source]

Add dunder methods based on the fields defined in the class.

Examines PEP 526 __annotations__ to determine fields.

If init is true, an __init__() method is added to the class. If repr is true, a __repr__() method is added. If order is true, rich comparison dunder methods are added. If unsafe_hash is true, a __hash__() method is added. If frozen is true, fields may not be assigned to after instance creation. If match_args is true, the __match_args__ tuple is added. If kw_only is true, then by default all fields are keyword-only. If slots is true, a new class with a __slots__ attribute is returned.

class novelai_image_mcp.nai.response.Any(*args, **kwargs)[source]

Bases: object

Special type indicating an unconstrained type.

  • Any is assignable to every type.

  • Any assumed to have all methods and attributes.

  • All values are assignable to Any.

Note that all the above statements are true from the point of view of static type checkers. At runtime, Any cannot be used with instance checks.

novelai_image_mcp.nai.response.cast(typ, val)[source]

Cast a value to a type.

This returns the value unchanged. To the type checker this signals that the return value has the designated type, but at runtime we intentionally don’t check anything (we want this to be as fast as possible).

novelai_image_mcp.nai.response.explain(code)[source]

Return NovelAI’s official explanation for a documented error code.

Undocumented codes (including API-supplied MessagePack stream codes) return None — the API’s own message is then the best explanation.

Parameters:

code (int | str | None)

Return type:

str | None

novelai_image_mcp.nai.response.parse_error_body(content)[source]

Extract (code, message) from a NovelAI error body.

NovelAI returns JSON in several shapes — {"statusCode", "message"}, {"code", "message"}, {"error": ...}, or a bare {"message"} — or, on some legacy paths, raw text. Falls back to the decoded text when neither a code nor a message field is present.

Parameters:

content (bytes)

Return type:

ErrorInfo

class novelai_image_mcp.nai.response.NovelAIImage(filename: 'str', data: 'bytes')[source]

Bases: object

Parameters:
filename: str
data: bytes
class novelai_image_mcp.nai.response.GenerationEvent(event_type: 'str', sample_index: 'int', step_index: 'int', generation_id: 'str', sigma: 'float', image: 'NovelAIImage')[source]

Bases: object

Parameters:
event_type: str
sample_index: int
step_index: int
generation_id: str
sigma: float
image: NovelAIImage
novelai_image_mcp.nai.response.check_status(status_code, content)[source]

Raise the domain error associated with an HTTP status.

The NovelAI error code (body statusCode/code, or the HTTP status when the body carries none) and its official explanation are attached to the raised exception and appended to its message.

Parameters:
Return type:

None

novelai_image_mcp.nai.response.parse_zip_images(content)[source]
Parameters:

content (bytes)

Return type:

tuple[NovelAIImage, …]

class novelai_image_mcp.nai.response.MessagePackStreamParser[source]

Bases: object

Incrementally parse NovelAI’s big-endian length-prefixed MessagePack.

feed(chunk)[source]
Parameters:

chunk (bytes)

Return type:

tuple[GenerationEvent, …]

finish()[source]
Return type:

None

novelai_image_mcp.nai.response.parse_messagepack_events(content)[source]
Parameters:

content (bytes)

Return type:

tuple[GenerationEvent, …]

novelai_image_mcp.nai.response.parse_messagepack_images(content)[source]
Parameters:

content (bytes)

Return type:

tuple[NovelAIImage, …]

Note

The exception classes listed above are imported into response for raising in check_status; they are documented under their defining module, novelai_image_mcp.nai.exceptions (see the next subsection).

novelai_image_mcp.nai.exceptions

Domain exception hierarchy:

Typed NovelAI domain failures.

Every exception optionally carries the NovelAI error code (HTTP status or API-supplied stream code) and, when documented, NovelAI’s official explanation — see nai.errors. The code and explanation are appended to the message (so they reach MCP tool callers and the CLI) and exposed as attributes for programmatic handling.

exception novelai_image_mcp.nai.exceptions.NovelAIError(message, *, code=None, explanation=None)[source]

Bases: Exception

Base class for NovelAI failures.

code is the NovelAI error code when known (None for transport, timeout, or local validation failures that carry no server code); explanation is NovelAI’s official explanation for documented codes.

Parameters:
  • message (str)

  • code (int | str | None)

  • explanation (str | None)

Return type:

None

exception novelai_image_mcp.nai.exceptions.NovelAIProviderError(message, *, code=None, explanation=None)[source]

Bases: NovelAIError

NovelAI rejected or failed the request.

Parameters:
  • message (str)

  • code (int | str | None)

  • explanation (str | None)

Return type:

None

exception novelai_image_mcp.nai.exceptions.NovelAIValidationError(message, *, code=None, explanation=None)[source]

Bases: NovelAIProviderError

The request is invalid.

Parameters:
  • message (str)

  • code (int | str | None)

  • explanation (str | None)

Return type:

None

exception novelai_image_mcp.nai.exceptions.NovelAIAuthenticationError(message, *, code=None, explanation=None)[source]

Bases: NovelAIProviderError

Credentials are missing, invalid, or expired.

Parameters:
  • message (str)

  • code (int | str | None)

  • explanation (str | None)

Return type:

None

exception novelai_image_mcp.nai.exceptions.NovelAIInsufficientCreditsError(message, *, code=None, explanation=None)[source]

Bases: NovelAIProviderError

The account has insufficient Anlas or no subscription.

Parameters:
  • message (str)

  • code (int | str | None)

  • explanation (str | None)

Return type:

None

exception novelai_image_mcp.nai.exceptions.NovelAIConcurrencyError(message, *, code=None, explanation=None)[source]

Bases: NovelAIProviderError

The account hit a concurrency or rate limit.

Parameters:
  • message (str)

  • code (int | str | None)

  • explanation (str | None)

Return type:

None

exception novelai_image_mcp.nai.exceptions.NovelAITimeoutError(message, *, code=None, explanation=None)[source]

Bases: NovelAIError

A request exceeded its timeout.

Parameters:
  • message (str)

  • code (int | str | None)

  • explanation (str | None)

Return type:

None

exception novelai_image_mcp.nai.exceptions.NovelAITransportError(message, *, code=None, explanation=None)[source]

Bases: NovelAIError

The request could not reach NovelAI.

Parameters:
  • message (str)

  • code (int | str | None)

  • explanation (str | None)

Return type:

None

exception novelai_image_mcp.nai.exceptions.NovelAIResponseError(message, *, code=None, explanation=None)[source]

Bases: NovelAIError

NovelAI returned malformed or unsupported data.

Parameters:
  • message (str)

  • code (int | str | None)

  • explanation (str | None)

Return type:

None

exception novelai_image_mcp.nai.exceptions.NovelAIImageError(message, *, code=None, explanation=None)[source]

Bases: NovelAIError

An input or output image is malformed or unsupported.

Parameters:
  • message (str)

  • code (int | str | None)

  • explanation (str | None)

Return type:

None

novelai_image_mcp.nai.errors

Error-body parsing and official error-code explanations: ErrorInfo, OFFICIAL_CODE_EXPLANATIONS, explain, parse_error_body.

NovelAI error-body parsing and official error-code explanations.

Every error raised toward NovelAI’s API carries the error code and — where documented — NovelAI’s official explanation text (see OFFICIAL_CODE_EXPLANATIONS). Codes sent inside MessagePack stream errors by the API itself ({code, message} events) are passed through verbatim.

novelai_image_mcp.nai.errors.OFFICIAL_CODE_EXPLANATIONS: dict[int, str] = {400: 'invalid request (validation failed)', 401: 'authentication failed (missing, invalid, or expired credentials)', 402: 'active subscription or Anlas required', 409: 'request conflicted with server state', 429: 'rate limit or concurrency limit reached'}

Official error codes documented by NovelAI’s API docs (https://api.novelai.net/docs) and the official FAQ. 5xx are deliberately folded into the generic provider-error bucket rather than enumerated.

class novelai_image_mcp.nai.errors.ErrorInfo(code, message, raw)[source]

Bases: object

Normalized error payload extracted from a NovelAI error body.

Parameters:
code: int | str | None
message: str
raw: str
novelai_image_mcp.nai.errors.explain(code)[source]

Return NovelAI’s official explanation for a documented error code.

Undocumented codes (including API-supplied MessagePack stream codes) return None — the API’s own message is then the best explanation.

Parameters:

code (int | str | None)

Return type:

str | None

novelai_image_mcp.nai.errors.parse_error_body(content)[source]

Extract (code, message) from a NovelAI error body.

NovelAI returns JSON in several shapes — {"statusCode", "message"}, {"code", "message"}, {"error": ...}, or a bare {"message"} — or, on some legacy paths, raw text. Falls back to the decoded text when neither a code nor a message field is present.

Parameters:

content (bytes)

Return type:

ErrorInfo

novelai_image_mcp.nai.service

create_novelai_client factory + NovelAIConfigLike protocol. The factory takes a settings-like object and an httpx.AsyncClient, returns a fully wired NovelAIClient.

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.

class novelai_image_mcp.nai.service.NovelAIConfigLike(*args, **kwargs)[source]

Bases: 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
legacy_image_base_url: str
timeout: float
vibe_cache_entries: int
novelai_image_mcp.nai.service.create_novelai_client(config, *, http_client=None)[source]

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.

Parameters:
Return type:

NovelAIClient

Official error codes

Every NovelAI error raised by this client carries the error code (the HTTP status, or the API’s own stream code) and, where documented, NovelAI’s official explanation. Both are appended to the message and exposed as attributes on the exception:

Code

Meaning (official)

400

Invalid request (validation failed)

401

Authentication failed

402

Active subscription or Anlas required

409

Request conflict

429

Rate limit / concurrency limit

5xx

NovelAI upstream error

All exceptions derive from NovelAIError and expose .code / .explanation (both None for transport, timeout, and local validation failures that carry no server code). See the novelai_image_mcp.nai.errors module for the parsing helpers.

See also