Source code for novelai_image_mcp.deps

"""Dependency-injected values shared by the MCP components.

FastMCP resolves parameters declared as ``Depends(get_...)`` (or annotated with
``Context``) when the component runs, and hides them from the component's input
schema. Tools, resources and prompts therefore declare exactly what they need
instead of digging into the request context themselves. ``AppContext`` is the
single value the server lifespan yields, and every provider reads from it.

The insiders/routing providers read the *request headers* instead: an HTTP
client opts into the experimental surface with the configured insiders header
and describes its route with the configured routing headers. Over stdio there is
no HTTP request at all, so the header lookup resolves to an empty mapping, the
insiders surface stays disabled and behaviour is unchanged.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING

from fastmcp.dependencies import CurrentContext, CurrentHeaders, Depends
from fastmcp.server.dependencies import get_context, get_http_request

from .features import MODEL_ROUTING, resolve_features
from .insiders import InsidersConfig, parse_insiders
from .routing import RoutingContext, resolve_routing
from .settings import get_mcp_settings

if TYPE_CHECKING:
    from fastmcp import Context

    from .nai import NovelAIClient
    from .settings import NovelAISettings


[docs] @dataclass class AppContext: """Shared state yielded by the lifespan to every MCP request.""" client: NovelAIClient settings: NovelAISettings
[docs] def get_app_context(ctx: Context = CurrentContext()) -> AppContext: """Return the lifespan ``AppContext`` for the current request.""" value = ctx.lifespan_context if not isinstance(value, AppContext): # pragma: no cover - defensive raise TypeError("the server lifespan did not yield an AppContext") return value
[docs] def get_client(app: AppContext = Depends(get_app_context)) -> NovelAIClient: """Return the shared ``NovelAIClient`` for the current request.""" return app.client
[docs] def get_insiders_config( headers: dict[str, str] = CurrentHeaders(), ) -> InsidersConfig: """Parse the insiders opt-in header for the current request. ``CurrentHeaders()`` resolves to an empty mapping when there is no HTTP request, so over stdio the insiders surface stays disabled and behaviour is unchanged. """ name = get_mcp_settings().insiders_header.strip().lower() return parse_insiders(headers.get(name))
def _effective_features(config: InsidersConfig) -> frozenset[str]: """Apply the operator-level kill switch to a parsed insiders config.""" return resolve_features( config, force_disabled=get_mcp_settings().insiders_disabled_features, )
[docs] def get_effective_features( config: InsidersConfig = Depends(get_insiders_config), ) -> frozenset[str]: """Return the names of the features active for this request. The header decides per request; ``MCP_INSIDERS_DISABLED_FEATURES`` is the operator kill switch and wins over anything a client sends. """ return _effective_features(config)
[docs] def get_route_scoped_output() -> bool: """True when generated images belong under a per-route subdirectory.""" return get_mcp_settings().route_scoped_output
def _client_info_name() -> str | None: """Return the MCP client's advertised name, or ``None`` when unavailable. ``get_context()`` raises outside a request, ``Context.session`` raises until the MCP session exists, and ``client_params`` is ``None`` for some transports. None of those is an error for routing, so every case degrades to ``None``. """ try: params = get_context().session.client_params except RuntimeError: return None if params is None: return None return params.client_info.name or None def _has_http_request() -> bool: """True when the current call arrived over HTTP rather than stdio.""" try: get_http_request() except RuntimeError: return False return True
[docs] def get_routing_context( headers: dict[str, str] = CurrentHeaders(), ) -> RoutingContext: """Resolve the routing context for the current request. Routing is attempted only when the ``model-routing`` feature is active; otherwise the disabled context is returned and every routing field stays ``None``. """ settings = get_mcp_settings() config = get_insiders_config(headers) return resolve_routing( headers, enabled=MODEL_ROUTING in _effective_features(config), model_headers=settings.model_headers, agent_headers=settings.agent_headers, client_headers=settings.client_headers, client_info_name=_client_info_name(), has_http_request=_has_http_request(), )
__all__ = [ "AppContext", "get_app_context", "get_client", "get_effective_features", "get_insiders_config", "get_route_scoped_output", "get_routing_context", ]