В статье Spring AI: retrieval augmented generation мы научились добавлять в контекст модели произвольные данные из векторного хранилища. Теперь давайте пойдём ещё дальше и посмотрим, как можно добавлять в контекст модели сторонние инструменты.

Протокол контекста модели (Model Context Protocol, MCP) — это открытый стандарт, разработанный и представленный компанией Anthropic 25 ноября 2024 года. Основная цель MCP — создание унифицированного протокола взаимодействия между большими языковыми моделями (LLM) и внешними источниками данных и инструментами. MCP унифицирует определения вызовов интерфейса для доступа к возможностям различных инструментов.
Архитектура с использованием MCP состоит из MCP-клиента, который обращается к одному или нескольким MCP-серверам. Эти сервера интегрированы с целевыми инструментами и источниками данных. Spring AI позволяет выполнить эту интеграцию в простом декларативном стиле. Вам даже не потребуется разбираться с протоколом, т.к. Spring будет генерить описания инструментов автоматически. Также MCP-клиент одновременно является связующим звеном с LLM.

Какие инструменты можно интегрировать в контекст LLM? Давайте рассмотрим простой пример. При этом он очень хорошо иллюстрирует плюсы, которые вы получаете от использования MCP.
Как известно, модель в общем случае вещь статическая. Её тренировали на каком-то наборе данных, который был актуальным на определённую дату. Отсюда следует, что LLM не обладает текущим контекстом времени. Если мы спросим LLM, сколько сейчас времени и��и какой сегодня день, она вам ответить не сможет или что-то попытается нафантазировать. Но мы можем создать MCP-инструмент, возвращающий текущую дату и время.
MCP-сервер
Создадим пустой спринговый проект (например, с помощью Spring Initializr). Выбираем тип проекта - Gradle-Kotlin, язык - Kotlin и версию Java - 21. Из зависимостей добавим только spring-ai-starter-mcp-server-webmvc. В качестве альтернативы также можно использовать spring-ai-starter-mcp-server-webflux, если вы хотите использовать неблокирующий стек. И тот, и другой стартер автоматически поднимает mcp-сервер и делает доступными для интеграции все методы, помеченные специальными аннотациями. Пример MCP-сервера и MCP-клиента вы можете посмотреть на моём github.
Чтобы в дальнейшем запускать и mcp-сервер и mcp-клиент на одной машине, давайте сразу в application.yml переопределим дефолтный порт на 8081.
server:
port: 8081
Теперь создадим спринговый сервис и в нём метод, возвращающий текущую дату и время.
@Service
class ToolService {
@Tool(description = "Получить текущую дату и время.")
fun getTime(): LocalDateTime {
val now = LocalDateTime.now()
println("Now: $now")
return now
}
}
Этот метод мы снабжаем аннотацией @Tool, которая как раз и указывает, что данный метод должен быть доступен как MCP-инструмент. В параметрах аннотации обязательно определяем description - именно на это описание будет опираться LLM, чтобы понять, какой именно метод ей нужно вызывать.
Вторым шагом нам нужно определить бин toolsProvider, в котором будут перечислены все mcp-сервисы.
@Configuration
class ToolConfig {
@Bean
fun toolsProvider(toolService: ToolService): ToolCallbackProvider =
MethodToolCallbackProvider.builder()
.toolObjects(toolService)
.build()
}
Созданный выше toolService мы сюда инжектим через параметр стандартными средствами Spring. Внутри конструируем ToolCallbackProvider с помощью билдера, подставляя этот сервис.
Теперь запускаем приложение и если всё сделано правильно, в логах увидим сообщение "Registered tools: 1". Наш MCP-сервер готов!
MCP-клиент
Создадим второе приложение с помощью Spring Initializr. Добавляем в проект 3 зависимости: Spring Web, OpenAI и Model Context Protocol Client.

Как и в предыдущих статьях, в application.yml настраиваем параметры подключения к LLM.
spring:
ai:
openai:
api-key: ${OPEN_AI_API_KEY}
base-url: ${OPEN_AI_BASE_URL:https://api.openai.com}
Для подключения к OpenAI нам потребуется Api-Key, который можно сгенерировать в личном кабинете https://platform.openai.com/api-keys. Также, если вы подключаетесь через какой-либо прокси-сервис OpenAI или используете любую другую LLM с совместимым протоколом, надо ещё прописать целевой хост в переменной OPEN_AI_BASE_URL. Если же подключаетесь напрямую - этот параметр можно вообще не указывать и будет использовано значение по умолчан��ю.
Далее пропишем MCP-сервер (их может быть несколько, но в нашем случае один).
spring:
ai:
# ...
mcp:
client:
sse:
connections:
mcp-server-example:
url: http://localhost:8081
Каждому серверу мы присваиваем произвольное имя (в данном случае mcp-server-example) и прописываем его урл. Обратите внимание, что тут мы указываем ровно тот порт, который переопределили выше, т.е. 8081. Взаимодействие с MCP-серверами происходит через SSE (server-side events). Каждый сервер должен предоставлять определённый эндпоинт, куда клиент будет слать сообщения. Но всё это взаимодействие берёт на себя Spring.
Конфигурация chatClient
Теперь создадим конфигурацию с бином chatClient, который позволяет взаимодействовать с LLM в диалоговом режиме.
@Configuration
class AiConfig {
@Bean
fun chatClient(
builder: ChatClient.Builder,
toolCallbackProvider: ToolCallbackProvider,
): ChatClient =
builder
.defaultAdvisors(
SimpleLoggerAdvisor(),
)
.defaultToolCallbacks(toolCallbackProvider)
.build()
}
Помимо уже традиционного SimpleLoggerAdvisor, который логирует запрос и ответ LLM, добавляем с помощью метода defaultToolCallback() бин toolCallbackProvider. Именно этот бин представляет собой реестр всех MCP-инструментов, доступных для LLM.
Сервис для взаимодействия с LLM
Теперь создадим AiService, который отвечает за взаимодействие с LLM.
@Service
class AiService(
private val chatClient: ChatClient,
) {
fun processUserMessage(userMessage: String): String {
// ...
}
}
В этот сервис подтягиваем chatClient, который сконфигурировали выше. Создадим здесь единственный метод processUserMessage(), принимающий текстовый запрос от пользователя и возвращающий ответ от LLM.
fun processUserMessage(userMessage: String): String {
val responseFormat = ResponseFormat.builder()
.type(ResponseFormat.Type.TEXT)
.build()
val chatOptions = OpenAiChatOptions.builder() // или другая реализация ToolCallingChatOptions
.model(OpenAiApi.ChatModel.GPT_4_1_MINI)
.temperature(0.0)
.responseFormat(responseFormat)
.build()
return chatClient.prompt(Prompt(SystemMessage(SYSTEM_PROMPT), chatOptions))
.user(userMessage)
.call()
.content()
?: "Не удалось получить ответ"
}
Внутри этого метода делаем всё очень похожим образом, как мы делали в других статьях про Spring AI: указываем, что ответ ожидается в виде текста без форматирования, затем указываем целевую модель и температуру выставляем в 0, чтобы ответы были максимально точными. Затем указываем какой-то системный промт с базовыми инструкциями для LLM и передаём сюда chatOptions.
Тут важно отметить, что для корректной работы MCP сюда нужно передавать не просто какой-то объект, реализующий ChatOptions, а его более частный случай - ToolCallingChatOptions. Если сделаете иначе - ошибки не будет, но и MCP не заработает. Благо OpenAiChatOptions реализует нужный нам интерфейс, а также поддерживает перечисление OpenAiApi.ChatModel со всеми доступными в OpenAI моделями.
Тестируем работу MCP
В целях демонстрации создадим rest-контроллер, чтобы взаимодействовать с LLM.
@RestController
@RequestMapping("/ai")
class AiController(
private val aiService: AiService,
) {
@PostMapping("/tools")
fun processUserMessage(@RequestBody message: MessageDto): MessageDto =
MessageDto(
text = aiService.processUserMessage(message.text)
)
}
Теперь можно приступать к тестированию. Сначала запускаем mcp-сервер, затем mcp-клиент. С помощью Postman отправляем POST-запрос на эндпоинт http://127.0.0.1:8080/ai/tools.

В это время в логах mcp-сервера мы также увидим значение текущего времени. То есть LLM действительно выполнила запрос нашего инструмента.
Вы можете закомментировать строку с вызовом defaultToolCallbacks() в конфигурации chatClient и ещё раз спросить время. MCP-сервер выйдет из контекста и нейросеть ответит ожидаемо.

MCP-метод с параметрами
Получение текущего времени не предполагает наличие каких-то параметров запроса. Давайте сделаем более комплексный пример и создадим метод, который будет создавать напоминание с определённым текстом на определённое время. Здесь нас не интересует сама логика создания такого напоминания, а только факт вызова метода и параметры, которые придут на вход.
Добавим метод createReminder() в сервис ToolService в mcp-сервере:
@Service
class ToolService {
// ...
@Tool(description = "Создать напоминание на определённое время.")
fun createReminder(
@ToolParam(description = "Текст напоминания")
description: String,
@ToolParam(description = "Время срабатывания напоминания")
dateTime: LocalDateTime,
): String {
// логика создания напоминания
val message = "Напоминание с текстом '$description' сработает $dateTime."
println(message)
return message
}
}
Тут мы снабжаем описанием не только сам метод, но и каждый его параметр с помощью аннотаций @ToolParam. Эти описания также крайне важны, чтобы LLM понимала, что от неё ожидается.
Теперь мы можем попросить LLM, чтобы она создала напоминание. Причём мы можем указывать не абсолютное время, а относительное. Это вынудит LLM сначала узнать текущее время с помощью метода, который мы сделали ранее.

Тут мы попросили создать напоминание и указали время его срабатывания как +2 часа от текущего. В логах сервера убеждаемся, что были вызваны оба метода:
Now: 2025-06-23T11:34:44.300088719
Напоминание с текстом 'Купить хлеб' сработает 2025-06-23T13:34:44.
Комплексный пример с тремя методами
Прибавить два часа к текущему времени - это далеко не всё, на что способна LLM. Давайте создадим ещё более комплексный пример, в котором LLM будет принимать решение. Например, мы хотим купить хлеб по самой низкой цене. Для этого добавим третий метод, возвращающий цены на хлеб в разных магазинах.
@Service
class ToolService {
// ...
@Tool(description = "Получить цены на хлеб в разных магазинах.")
fun getBreadPrices(): Map<String, BigDecimal> {
println("Получение цен на хлеб в разных магазинах.")
return mapOf(
"Лента" to BigDecimal("60.00"),
"Пятёрочка" to BigDecimal("50.00"),
"Азбука Вкуса" to BigDecimal("100.00"),
)
}
}
Метод просто возвращает мапу, где ключом является название магазина, а значением - цена на хлеб.
Тогда LLM должна выяснить название магазина с самой низкой ценой, затем запросить текущее время и создать соответствующее напоминание.

В логах mcp-сервера мы увидим, что были вызваны все три метода в правильной последовательности:
Получение цен на хлеб в разных магазинах.
Now: 2025-06-23T12:39:12.838160949
Напоминание с текстом 'Купить хлеб в магазине Пятёрочка' сработает 2025-06-24T09:00.
Заключение
Spring AI предоставляет очень простой декларативный подход для добавления любых инструментов в контекст LLM с помощью Model Context Protocol. Вам даже не требуется разбираться с форматом этого протокола. Однако очень важно делать подробные описания методов и их параметров, чтобы LLM понимала, что ей требуется вызывать и в какой последовательности.
Рассмотренные в статье примеры MCP-сервера
и MCP-клиента содержат Dockerfile и полностью готовы к деплою.
Ещё больше статей по разработке на Java, Kotlin и Spring вы найдёте на моём сайте.
