Перейти к основному содержимому

Внедрение зависимостей

Neva включает встроенный контейнер внедрения зависимостей (DI), позволяющий регистрировать общие сервисы — подключения к базам данных, HTTP-клиенты, объекты конфигурации, кэши — и автоматически предоставлять их обработчикам инструментов, ресурсов и промптов.

к сведению

DI входит в пресет server-full. При использовании пользовательского набора компонентов добавьте компонент di явно:

neva = { version = "...", features = ["server-macros", "di"] }

Жизненные циклы сервисов

Neva поддерживает три жизненных цикла сервисов, управляющих тем, как и когда создаются экземпляры:

Жизненный циклСоздаётсяОбщий для
SingletonОдин раз при запускеВсех запросов и сессий
ScopedОдин раз на входящее MCP-сообщениеВсех обработчиков в рамках одного запроса
TransientПри каждом разрешенииНичего — каждый вызов получает новый экземпляр

Выбирайте singleton для stateless или потокобезопасных сервисов (например, HTTP-клиент или конфигурация только для чтения). Выбирайте scoped, когда сервис должен быть общим в рамках одного запроса, но изолированным от других (например, транзакция базы данных). Выбирайте transient, когда всегда нужен новый экземпляр.

Регистрация сервисов

Сервисы регистрируются в App при настройке, до вызова .run().

Singleton

Передайте уже созданный экземпляр:

use neva::prelude::*;

#[derive(Clone)]
struct AppConfig {
api_url: String,
}

#[tokio::main]
async fn main() {
let config = AppConfig { api_url: "https://api.example.com".into() };

App::new()
.with_options(|opt| opt.with_stdio())
.add_singleton(config)
.run()
.await;
}

T должен реализовывать Send + Sync + 'static. Clone необходим для прямого извлечения значения; для совместного использования по указателю используйте Dc<T> (описано ниже).

Scoped — через трейт Inject

Реализуйте трейт Inject, чтобы описать, как сервис создаётся из контейнера. Контейнер вызывает это один раз на область видимости запроса.

use neva::prelude::*;

#[derive(Clone)]
struct RequestLogger {
prefix: String,
}

impl Inject for RequestLogger {
fn inject(_: &Container) -> Result<Self, DiError> {
Ok(Self { prefix: "[req]".into() })
}
}

App::new()
.with_options(|opt| opt.with_stdio())
.add_scoped::<RequestLogger>()
.run()
.await;

Scoped — через фабрику

Когда конструирование простое или не хочется реализовывать Inject, передайте замыкание:

App::new()
.add_scoped_factory(|| RequestLogger { prefix: "[req]".into() })
.run()
.await;

Scoped — через Default

Если ваш тип реализует Default, используйте сокращённую форму:

#[derive(Default, Clone)]
struct RequestContext {
trace_id: Option<String>,
}

App::new()
.add_scoped_default::<RequestContext>()
.run()
.await;

Transient — через Inject, фабрику или Default

Варианты transient аналогичны scoped. Единственное отличие: фабрика (или Inject::inject, или Default::default) вызывается каждый раз при разрешении типа, а не один раз на область видимости:

App::new()
.add_transient::<MyService>() // через Inject
.add_transient_factory(|| MyService::new()) // через замыкание
.add_transient_default::<MyService>() // через Default
.run()
.await;

Извлечение сервисов в обработчиках

Используйте Dc<T> как параметр функции для получения сервиса в любом обработчике инструмента, ресурса или промпта. Dc (Dependency Container) оборачивает разрешённый экземпляр в Arc и реализует Deref, поэтому его можно использовать как обычную ссылку.

use neva::prelude::*;

#[derive(Default, Clone)]
struct AppConfig {
greeting: String,
}

#[tool(descr = "Greets a user using configured greeting")]
async fn hello(config: Dc<AppConfig>, name: String) -> String {
format!("{}, {name}!", config.greeting)
}

#[tokio::main]
async fn main() {
let config = AppConfig { greeting: "Hello".into() };

App::new()
.with_options(|opt| opt.with_stdio())
.add_singleton(config)
.run()
.await;
}

Dc<T> работает со всеми типами обработчиков:

// В обработчике ресурса
#[resource(uri = "data://{id}", title = "Fetch data")]
async fn fetch_data(db: Dc<Database>, uri: Uri, id: String) -> ResourceContents {
let row = db.get(&id).await?;
ResourceContents::new(uri).with_text(row)
}

// В обработчике промпта
#[prompt(descr = "Generate a prompt using config")]
async fn my_prompt(config: Dc<AppConfig>, topic: String) -> PromptMessage {
PromptMessage::user().with(format!("{}: {topic}", config.greeting))
}

Получение значения в собственность

Если нужен T во владение, а не общая ссылка, вызовите .cloned() на Dc<T>:

#[tool(descr = "Returns config details")]
async fn describe(config: Dc<AppConfig>) -> String {
let owned: AppConfig = config.cloned();
owned.greeting
}

Извлечение сервисов в промежуточных обработчиках

Внутри промежуточного обработчика используйте ctx.resolve::<T>() для получения клонированного значения или ctx.resolve_shared::<T>() для Arc<T>:

use neva::prelude::*;

async fn auth_middleware(ctx: MwContext, next: Next) -> Response {
let config = ctx.resolve_shared::<AppConfig>()?;
// Использование config для проверки аутентификации, логирования и т.д.
next(ctx).await
}
примечание

resolve и resolve_shared доступны только при включённом компоненте di.

Трейт Inject

Inject даёт типу возможность извлекать свои собственные зависимости из контейнера — полезно, когда сервис сам зависит от других зарегистрированных сервисов:

use neva::prelude::*;

#[derive(Clone)]
struct EmailService {
config: AppConfig,
}

impl Inject for EmailService {
fn inject(container: &Container) -> Result<Self, DiError> {
let config = container.resolve::<AppConfig>()?;
Ok(Self { config })
}
}

При вызове add_scoped::<EmailService>() Neva будет вызывать EmailService::inject(container) в начале каждой области видимости запроса.

Полный пример

use neva::prelude::*;

// --- Сервисы ---

#[derive(Clone)]
struct AppConfig {
api_url: String,
}

#[derive(Clone)]
struct ApiClient {
base_url: String,
}

impl ApiClient {
async fn fetch(&self, path: &str) -> String {
format!("GET {}{path}", self.base_url)
}
}

impl Inject for ApiClient {
fn inject(container: &Container) -> Result<Self, DiError> {
let config = container.resolve::<AppConfig>()?;
Ok(Self { base_url: config.api_url.clone() })
}
}

// --- Обработчики ---

#[tool(descr = "Fetches data from the upstream API")]
async fn fetch_data(client: Dc<ApiClient>, path: String) -> String {
client.fetch(&path).await
}

// --- Главная функция ---

#[tokio::main]
async fn main() {
let config = AppConfig { api_url: "https://api.example.com".into() };

App::new()
.with_options(|opt| opt
.with_stdio()
.with_name("API MCP server")
.with_version("1.0.0"))
.add_singleton(config) // общий, заранее созданный
.add_scoped::<ApiClient>() // создаётся один раз на запрос через Inject
.run()
.await;
}