Architecture¶
The server is a thin composition root over a NovelAI HTTP client. This page walks through the layers and how they fit together.
Layers¶
graph TD
A[Agent / MCP host] -->|JSON-RPC over stdio / HTTP| B[MCPServer]
B -->|lifespan| C[AppContext: NovelAIClient + NovelAISettings]
B -->|tool calls| D[tools/*]
D -->|calls| E[NovelAIClient nai/]
E -->|httpx.AsyncClient| F[NovelAI API]
D -->|saves| G[output.py → outputs/]
MCPServer composition root¶
novelai_image_mcp.server is the composition root. It:
Builds a single shared
httpx.AsyncClient(connection pool) owned by the MCPlifespan.Builds a
NovelAIClientfrom settings + the httpx client.Yields both via
AppContextto every tool invocation.Tears down both on shutdown (closing
NovelAIClientfirst, then the underlying httpx session — even if the client close raises, the httpx session is still released to avoid leaking sockets).
@asynccontextmanager
async def lifespan(_server: MCPServer) -> AsyncIterator[AppContext]:
settings = get_novelai_settings()
if not settings.has_credentials():
raise RuntimeError("NovelAI credentials are not configured...")
http_client = httpx.AsyncClient(timeout=settings.timeout)
client = create_novelai_client(settings, http_client=http_client)
try:
yield AppContext(client=client, settings=settings)
finally:
try:
await client.aclose()
finally:
if not http_client.is_closed:
await http_client.aclose()
Every tool reads the client + settings from the MCP request context:
app = _app(ctx) # extracts AppContext from ctx.request_context.lifespan_context
settings = app.settings
client = app.client
NovelAIClient (the nai/ subpackage)¶
The nai/ subpackage is the NovelAI HTTP client. It is agnostic of
MCP — it can be used standalone from any async Python code.
Module |
Responsibility |
|---|---|
|
Argon2id access-key derivation; request tracking headers. |
|
|
|
Enums: |
|
Domain exceptions: |
|
Pydantic models: |
|
|
|
|
|
|
Tools layer¶
novelai_image_mcp.tools is a thin adapter between the MCP MCPServer
tool decorator and NovelAIClient. Each tool:
Extracts
AppContextfrom the MCP context (tools/_ctx.py).Builds a
GenerationRequest(or equivalent) from tool parameters.Calls the corresponding
NovelAIClientmethod.Persists the result via
output.save_image.Returns a list of MCP content blocks:
[Image, text_path].
Tools deliberately avoid business logic — validation and parameter
marshaling live in nai/ so the same code path serves the MCP server, the
CLI, and direct client use.
CLI¶
novelai_image_mcp.cli is a typer app that exposes a subset of the MCP
tools as direct subcommands (generate, upscale, director, annotate,
info, serve). It constructs a short-lived httpx.AsyncClient +
NovelAIClient per invocation — no shared session. For long-lived
multi-tool use, prefer the MCP server (which owns a pooled session via its
lifespan).
Configuration¶
novelai_image_mcp.settings defines two pydantic-settings models:
NovelAISettings— credentials, endpoints, generation defaults (NOVELAI_*env vars).MCPServerSettings— transport selection (MCP_*env vars).
Both load from process env + .env (in cwd), UTF-8, case-insensitive,
unknown keys ignored. NovelAISettings structurally satisfies
NovelAIConfigLike, so the client factory consumes it directly.
Output¶
novelai_image_mcp.output.save_image writes a PNG to
NOVELAI_OUTPUT_DIR, returning the path. The directory is created on
demand. Filenames include the tool name + ISO timestamp + sample index
(generate-20260725-133702-001.png).
Transports¶
The server supports stdio (default) and streamable-http, selected at
startup via MCP_TRANSPORT. Both share the same lifespan, the same
tools, and the same NovelAIClient — only the framing differs. See
Transports.
Containerization¶
apps/server/Dockerfile
is a multi-stage build:
Builder stage: installs uv, exports runtime deps (no dev groups), builds wheels.
Project build stage: builds the project wheel via
uv build.Runtime stage: slim Python 3.13, non-root
appuser, installs pre-built wheels only — no compiler toolchain in the runtime image.
The smoke test (apps/server/docker/smoke-test.py) is built into a
separate image (via the SMOKE_TEST=true build-arg) and verifies:
The package imports cleanly.
Settings instantiate from env (with
NOVELAI_TOKEN=pst-smoke-test).Every tool registers against a recording MCPServer stub.
The typer CLI constructs without runtime errors.