"""MCP tools: upscale, Director tools, and ControlNet annotation."""
from __future__ import annotations
import base64
from pathlib import Path
from typing import Annotated, Any, Literal
from fastmcp.dependencies import Depends
from fastmcp.exceptions import ToolError
from fastmcp.utilities.types import Image
from pydantic import Field
from ..deps import (
AppContext,
get_app_context,
get_effective_features,
get_route_scoped_output,
get_routing_context,
)
from ..features import EXPERIMENTAL_TOOLS
from ..nai import (
ControlNetModel,
DirectorTool,
Emotion,
EmotionLevel,
Model,
NovelAIImage,
)
from ..output import route_output_dir, save_image
from ..routing import RoutingContext
from ..server import mcp
from ._errors import translate_errors
from ._meta import IMAGE_WRITE_ANNOTATIONS
#: NovelAI's line-art sharpening parameter is clamped to this range.
_MAX_DEFRY = 10
def _save_and_return(
image: NovelAIImage,
*,
name: str,
output_dir: str | Path,
routing: RoutingContext,
) -> list[Any]:
"""Persist a single image and return an ImageContent block + saved path.
FastMCP auto-converts its `Image` helper into an `ImageContent` block
when returned from a tool, so returning `Image(...)` needs no manual
conversion step. When routing is active the returned text attributes the
saved path to the request's route.
"""
path = save_image(image.data, name=name, output_dir=output_dir)
location = f" [route={routing.route}]" if routing.enabled else ""
return [
Image(data=image.data, format="png"),
f"Saved image{location}: {path}",
]
[docs]
@mcp.tool(
title="Upscale an image (4x)",
tags={"novelai", "enhancement"},
annotations=IMAGE_WRITE_ANNOTATIONS,
)
@translate_errors
async def upscale_image(
image: str,
factor: Literal[4] = 4,
routing: RoutingContext = Depends(get_routing_context),
route_scoped_output: bool = Depends(get_route_scoped_output),
app: AppContext = Depends(get_app_context),
) -> list[Any]:
"""Upscale an image 4x using NovelAI's standalone upscaler.
``image`` is a base64-encoded PNG/JPEG. NovelAI's standalone upscaler
offers a single 4x step — the API exposes no scaling factor — so
``factor`` only accepts ``4``. The upscaler runs on the V5 model
configured by ``NOVELAI_UPSCALE_MODEL`` (default
``nai-diffusion-5-full``) and consumes Anlas based on the source
resolution.
"""
settings = app.settings
client = app.client
result = await client.upscale(
base64.b64decode(image), factor=factor, model=Model(settings.upscale_model)
)
return _save_and_return(
result,
name="upscale",
output_dir=route_output_dir(
settings.output_dir, routing, route_scoped=route_scoped_output
),
routing=routing,
)
[docs]
@mcp.tool(
title="Annotate an image (ControlNet)",
tags={"novelai", "enhancement"},
annotations=IMAGE_WRITE_ANNOTATIONS,
)
@translate_errors
async def annotate_image(
image: str,
model: ControlNetModel,
routing: RoutingContext = Depends(get_routing_context),
route_scoped_output: bool = Depends(get_route_scoped_output),
app: AppContext = Depends(get_app_context),
) -> list[Any]:
"""Annotate an image with a ControlNet preprocessor.
``image`` is a base64-encoded PNG/JPEG. ``model`` is one of:
``hed`` (palette swap), ``midas`` (form lock / depth),
``fake_scribble`` (scribbler), ``mlsd`` (building control),
``uniformer`` (landscaper). The returned image is the annotation (e.g. a
line-art map) suitable for use as a ControlNet condition in a subsequent
generation.
"""
settings = app.settings
client = app.client
controlnet = ControlNetModel(model)
result = await client.annotate(base64.b64decode(image), controlnet)
return _save_and_return(
result,
name=f"annotate-{controlnet.value}",
output_dir=route_output_dir(
settings.output_dir, routing, route_scoped=route_scoped_output
),
routing=routing,
)
__all__ = ["annotate_image", "director_tool", "upscale_image"]