56 KiB
Створення клієнта з LLM
Досі ви бачили, як створити сервер і клієнта. Клієнт міг явно викликати сервер, щоб перелічити його інструменти, ресурси та підказки. Однак це не дуже практичний підхід. Ваші користувачі живуть у епоху агентних систем і очікують використовувати підказки та спілкуватися з LLM замість цього. Їх не цікавить, чи використовуєте ви MCP для збереження своїх можливостей; вони просто очікують взаємодії природною мовою. Тож як це вирішити? Рішення — додати LLM до клієнта.
Огляд
У цьому уроці ми зосередимося на додаванні LLM до клієнта та покажемо, як це забезпечує набагато кращий досвід для вашого користувача.
Цілі навчання
До кінця цього уроку ви зможете:
- Створити клієнта з LLM.
- Безперешкодно взаємодіяти з MCP сервером за допомогою LLM.
- Забезпечити кращий користувацький досвід на стороні клієнта.
Підхід
Давайте спробуємо зрозуміти, який підхід нам потрібно застосувати. Додавання LLM звучить просто, але чи справді ми це зробимо?
Ось як клієнт буде взаємодіяти із сервером:
-
Встановити з’єднання із сервером.
-
Перелічити можливості, підказки, ресурси та інструменти, зберегти їхню схему.
-
Додати LLM і передати збережені можливості та їхню схему у форматі, який розуміє LLM.
-
Обробити запит користувача, передаючи його до LLM разом з інструментами, переліченими клієнтом.
Чудово, тепер ми розуміємо, як це можна зробити на високому рівні, давайте спробуємо це в наведеному нижче завданні.
Завдання: Створення клієнта з LLM
У цьому завданні ми навчимося додавати LLM до нашого клієнта.
Аутентифікація за допомогою персонального токена доступу GitHub
Створення токена GitHub — це простий процес. Ось як ви можете це зробити:
- Перейдіть до налаштувань GitHub — натисніть на своє фото профілю у верхньому правому куті та виберіть Налаштування.
- Перейдіть до налаштувань розробника — прокрутіть вниз і натисніть Developer Settings.
- Виберіть Personal Access Tokens — натисніть Fine-grained tokens, а потім Generate new token.
- Налаштуйте свій токен — додайте примітку для довідки, встановіть дату закінчення дії та виберіть необхідні дозволи (scopes). Обов’язково додайте дозвіл Models.
- Згенеруйте та скопіюйте токен — натисніть Generate token і не забудьте скопіювати його відразу, бо потім побачити не зможете.
-1- Підключення до сервера
Спершу створимо наш клієнт:
TypeScript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import OpenAI from "openai";
import { z } from "zod"; // Імпортувати zod для валідації схеми
class MCPClient {
private openai: OpenAI;
private client: Client;
constructor(){
this.openai = new OpenAI({
baseURL: "https://models.inference.ai.azure.com",
apiKey: process.env.GITHUB_TOKEN,
});
this.client = new Client(
{
name: "example-client",
version: "1.0.0"
},
{
capabilities: {
prompts: {},
resources: {},
tools: {}
}
}
);
}
}
У наведеному вище коді ми:
- Імпортували потрібні бібліотеки
- Створили клас з двома членами,
clientтаopenai, які допомагатимуть нам управляти клієнтом і взаємодіяти з LLM відповідно. - Налаштували екземпляр LLM, щоб використовувати GitHub Models, задавши
baseUrlдля звернення до inference API.
Python
from mcp import ClientSession, StdioServerParameters, types
from mcp.client.stdio import stdio_client
# Створити параметри сервера для з’єднання stdio
server_params = StdioServerParameters(
command="mcp", # Виконуваний файл
args=["run", "server.py"], # Опціональні аргументи командного рядка
env=None, # Опціональні змінні оточення
)
async def run():
async with stdio_client(server_params) as (read, write):
async with ClientSession(
read, write
) as session:
# Ініціалізувати з’єднання
await session.initialize()
if __name__ == "__main__":
import asyncio
asyncio.run(run())
У наведеному вище коді ми:
- Імпортували потрібні бібліотеки для MCP
- Створили клієнта
.NET
using Azure;
using Azure.AI.Inference;
using Azure.Identity;
using System.Text.Json;
using ModelContextProtocol.Client;
using System.Text.Json;
var clientTransport = new StdioClientTransport(new()
{
Name = "Demo Server",
Command = "/workspaces/mcp-for-beginners/03-GettingStarted/02-client/solution/server/bin/Debug/net8.0/server",
Arguments = [],
});
await using var mcpClient = await McpClient.CreateAsync(clientTransport);
Java
Спершу потрібно додати залежності LangChain4j у файл pom.xml. Додайте ці залежності для підключення MCP і підтримки GitHub Models:
<properties>
<langchain4j.version>1.0.0-beta3</langchain4j.version>
</properties>
<dependencies>
<!-- LangChain4j MCP Integration -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- OpenAI Official API Client -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-official</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- GitHub Models Support -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-github-models</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- Spring Boot Starter (optional, for production apps) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
Потім створіть клас клієнта Java:
import dev.langchain4j.mcp.McpToolProvider;
import dev.langchain4j.mcp.client.DefaultMcpClient;
import dev.langchain4j.mcp.client.McpClient;
import dev.langchain4j.mcp.client.transport.McpTransport;
import dev.langchain4j.mcp.client.transport.http.HttpMcpTransport;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openaiofficial.OpenAiOfficialChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.tool.ToolProvider;
import java.time.Duration;
import java.util.List;
public class LangChain4jClient {
public static void main(String[] args) throws Exception { // Налаштуйте LLM для використання моделей GitHub
ChatLanguageModel model = OpenAiOfficialChatModel.builder()
.isGitHubModels(true)
.apiKey(System.getenv("GITHUB_TOKEN"))
.timeout(Duration.ofSeconds(60))
.modelName("gpt-4.1-nano")
.build();
// Створіть транспорт MCP для підключення до сервера
McpTransport transport = new HttpMcpTransport.Builder()
.sseUrl("http://localhost:8080/sse")
.timeout(Duration.ofSeconds(60))
.logRequests(true)
.logResponses(true)
.build();
// Створіть клієнт MCP
McpClient mcpClient = new DefaultMcpClient.Builder()
.transport(transport)
.build();
}
}
У наведеному вище коді ми:
- Додали залежності LangChain4j: необхідні для інтеграції MCP, офіційного OpenAI клієнта і підтримки GitHub Models
- Імпортували бібліотеки LangChain4j: для інтеграції MCP і функціоналу чат-моделі OpenAI
- Створили
ChatLanguageModel: налаштований на використання GitHub Models з вашим GitHub токеном - Налаштували HTTP-транспортування: за допомогою Server-Sent Events (SSE) для підключення до MCP сервера
- Створили клієнта MCP: який оброблятиме комунікацію із сервером
- Використали вбудовану підтримку MCP LangChain4j: що спрощує інтеграцію між LLM і MCP серверами
Rust
Цей приклад передбачає, що у вас вже працює MCP сервер на Rust. Якщо ні, зверніться до уроку 01-first-server, щоб створити сервер.
Коли у вас буде MCP сервер на Rust, відкрийте термінал і перейдіть до теки із сервером. Запустіть таку команду, щоб створити новий проект клієнта LLM:
mkdir calculator-llmclient
cd calculator-llmclient
cargo init
Додайте такі залежності до файлу Cargo.toml:
[dependencies]
async-openai = { version = "0.29.0", features = ["byot"] }
rmcp = { version = "0.5.0", features = ["client", "transport-child-process"] }
serde_json = "1.0.141"
tokio = { version = "1.46.1", features = ["rt-multi-thread"] }
Note
Офіційної бібліотеки Rust для OpenAI немає, але crate
async-openai— це спільно підтримувана бібліотека, яку часто використовують.
Відкрийте файл src/main.rs і замініть його вміст таким кодом:
use async_openai::{Client, config::OpenAIConfig};
use rmcp::{
RmcpError,
model::{CallToolRequestParam, ListToolsResult},
service::{RoleClient, RunningService, ServiceExt},
transport::{ConfigureCommandExt, TokioChildProcess},
};
use serde_json::{Value, json};
use std::error::Error;
use tokio::process::Command;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
// Початкове повідомлення
let mut messages = vec![json!({"role": "user", "content": "What is the sum of 3 and 2?"})];
// Налаштування клієнта OpenAI
let api_key = std::env::var("OPENAI_API_KEY")?;
let openai_client = Client::with_config(
OpenAIConfig::new()
.with_api_base("https://models.github.ai/inference/chat")
.with_api_key(api_key),
);
// Налаштування клієнта MCP
let server_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("calculator-server");
let mcp_client = ()
.serve(
TokioChildProcess::new(Command::new("cargo").configure(|cmd| {
cmd.arg("run").current_dir(server_dir);
}))
.map_err(RmcpError::transport_creation::<TokioChildProcess>)?,
)
.await?;
// TODO: Отримати список інструментів MCP
// TODO: Розмова LLM із викликами інструментів
Ok(())
}
Цей код налаштовує базовий додаток на Rust, який підключається до MCP сервера і використовує GitHub Models для взаємодії з LLM.
Important
Переконайтеся, що перед запуском додатка встановили змінну оточення
OPENAI_API_KEYзі своїм GitHub токеном.
Чудово, наступним кроком перелічимо можливості сервера.
-2- Перелічити можливості сервера
Тепер підключимося до сервера та запросимо його можливості:
Typescript
У тому ж класі додайте такі методи:
async connectToServer(transport: Transport) {
await this.client.connect(transport);
this.run();
console.error("MCPClient started on stdin/stdout");
}
async run() {
console.log("Asking server for available tools");
// перелік інструментів
const toolsResult = await this.client.listTools();
}
У наведеному коді ми:
- Додали код для підключення до сервера —
connectToServer. - Створили метод
run, відповідальний за керування потоком додатка. Поки що він просто перелічує інструменти, але скоро ми додамо більше.
Python
# Перелічте доступні ресурси
resources = await session.list_resources()
print("LISTING RESOURCES")
for resource in resources:
print("Resource: ", resource)
# Перелічте доступні інструменти
tools = await session.list_tools()
print("LISTING TOOLS")
for tool in tools.tools:
print("Tool: ", tool.name)
print("Tool", tool.inputSchema["properties"])
Ось що ми додали:
- Перелічення ресурсів і інструментів, а також їх вивід. Для інструментів ми також перелічуємо
inputSchema, що використовуватимемо пізніше.
.NET
async Task<List<ChatCompletionsToolDefinition>> GetMcpTools()
{
Console.WriteLine("Listing tools");
var tools = await mcpClient.ListToolsAsync();
List<ChatCompletionsToolDefinition> toolDefinitions = new List<ChatCompletionsToolDefinition>();
foreach (var tool in tools)
{
Console.WriteLine($"Connected to server with tools: {tool.Name}");
Console.WriteLine($"Tool description: {tool.Description}");
Console.WriteLine($"Tool parameters: {tool.JsonSchema}");
// TODO: convert tool definition from MCP tool to LLm tool
}
return toolDefinitions;
}
У наведеному коді ми:
- Перелічили інструменти, доступні на MCP сервері
- Для кожного інструменту перелічили назву, опис і схему. Остання знадобиться для виклику інструментів згодом.
Java
// Створіть постачальника інструментів, який автоматично знаходить інструменти MCP
ToolProvider toolProvider = McpToolProvider.builder()
.mcpClients(List.of(mcpClient))
.build();
// Постачальник інструментів MCP автоматично обробляє:
// - Перелік доступних інструментів з сервера MCP
// - Перетворення схем інструментів MCP у формат LangChain4j
// - Керування виконанням інструментів та відповідями
У наведеному коді ми:
- Створили
McpToolProvider, який автоматично знаходить і реєструє всі інструменти з MCP сервера - Провайдер інструментів самостійно перетворює схеми MCP інструментів у формат інструментів LangChain4j
- Такий підхід приховує деталі ручного переліку інструментів та їх перетворення
Rust
Отримання інструментів з MCP сервера здійснюється методом list_tools. Додайте у вашій функції main після налаштування MCP клієнта такий код:
// Отримати список інструментів MCP
let tools = mcp_client.list_tools(Default::default()).await?;
-3- Конвертувати можливості сервера в інструменти для LLM
Наступний крок після переліку можливостей сервера — конвертація їх у формат, який розуміє LLM. Після цього ми зможемо надати ці можливості як інструменти нашому LLM.
TypeScript
-
Додайте цей код для конвертації відповіді MCP сервера у формат інструментів, який може використовувати LLM:
openAiToolAdapter(tool: { name: string; description?: string; input_schema: any; }) { // Створіть схему zod на основі input_schema const schema = z.object(tool.input_schema); return { type: "function" as const, // Явно встановіть тип у "function" function: { name: tool.name, description: tool.description, parameters: { type: "object", properties: tool.input_schema.properties, required: tool.input_schema.required, }, }, }; }Наведений код перетворює відповідь MCP сервера у визначення інструменту в форматі, який розуміє LLM.
-
Оновимо метод
run, щоб перелічити можливості сервера:async run() { console.log("Asking server for available tools"); const toolsResult = await this.client.listTools(); const tools = toolsResult.tools.map((tool) => { return this.openAiToolAdapter({ name: tool.name, description: tool.description, input_schema: tool.inputSchema, }); }); }У наведеному коді ми оновили метод
run, щоб перебирати результат і для кожного запису викликатиopenAiToolAdapter.
Python
-
Спочатку створимо функцію конвертера:
def convert_to_llm_tool(tool): tool_schema = { "type": "function", "function": { "name": tool.name, "description": tool.description, "type": "function", "parameters": { "type": "object", "properties": tool.inputSchema["properties"] } } } return tool_schemaУ функції
convert_to_llm_toolsми приймаємо MCP інструмент і конвертуємо його у формат, що розуміє LLM. -
Далі оновимо код клієнта, щоб використовувати цю функцію:
functions = [] for tool in tools.tools: print("Tool: ", tool.name) print("Tool", tool.inputSchema["properties"]) functions.append(convert_to_llm_tool(tool))Тут ми викликаємо
convert_to_llm_toolдля перетворення відповіді MCP інструменту в формат, який можна подати LLM.
.NET
- Додамо код для перетворення відповіді MCP інструменту у щось зрозуміле для LLM:
ChatCompletionsToolDefinition ConvertFrom(string name, string description, JsonElement jsonElement)
{
// convert the tool to a function definition
FunctionDefinition functionDefinition = new FunctionDefinition(name)
{
Description = description,
Parameters = BinaryData.FromObjectAsJson(new
{
Type = "object",
Properties = jsonElement
},
new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })
};
// create a tool definition
ChatCompletionsToolDefinition toolDefinition = new ChatCompletionsToolDefinition(functionDefinition);
return toolDefinition;
}
У наведеному коді ми:
- Створили функцію
ConvertFrom, що приймає назву, опис і вхідну схему. - Визначили функціонал, який створює
FunctionDefinitionдля передачі уChatCompletionsDefinition— це те, що розуміє LLM.
-
Ось як оновити існуючий код, щоб використовувати цю функцію:
async Task<List<ChatCompletionsToolDefinition>> GetMcpTools() { Console.WriteLine("Listing tools"); var tools = await mcpClient.ListToolsAsync(); List<ChatCompletionsToolDefinition> toolDefinitions = new List<ChatCompletionsToolDefinition>(); foreach (var tool in tools) { Console.WriteLine($"Connected to server with tools: {tool.Name}"); Console.WriteLine($"Tool description: {tool.Description}"); Console.WriteLine($"Tool parameters: {tool.JsonSchema}"); JsonElement propertiesElement; tool.JsonSchema.TryGetProperty("properties", out propertiesElement); var def = ConvertFrom(tool.Name, tool.Description, propertiesElement); Console.WriteLine($"Tool definition: {def}"); toolDefinitions.Add(def); Console.WriteLine($"Properties: {propertiesElement}"); } return toolDefinitions; } ``` In the preceding code, we've: - Update the function to convert the MCP tool response to an LLm tool. Let's highlight the code we added: ```csharp JsonElement propertiesElement; tool.JsonSchema.TryGetProperty("properties", out propertiesElement); var def = ConvertFrom(tool.Name, tool.Description, propertiesElement); Console.WriteLine($"Tool definition: {def}"); toolDefinitions.Add(def); ``` The input schema is part of the tool response but on the "properties" attribute, so we need to extract. Furthermore, we now call `ConvertFrom` with the tool details. Now we've done the heavy lifting, let's see how it call comes together as we handle a user prompt next.
Java
// Створіть інтерфейс бота для взаємодії природною мовою
public interface Bot {
String chat(String prompt);
}
// Налаштуйте AI-сервіс з інструментами LLM та MCP
Bot bot = AiServices.builder(Bot.class)
.chatLanguageModel(model)
.toolProvider(toolProvider)
.build();
У наведеному коді ми:
- Визначили простий інтерфейс
Botдля взаємодії природною мовою - Використали
AiServicesLangChain4j для автоматичного зв’язування LLM з провайдером MCP інструментів - Фреймворк автоматично обробляє конвертацію схем інструментів і виклики функцій
- Цей підхід усуває ручне перетворення інструментів — LangChain4j справляється з усією складністю перетворення MCP інструментів у формат, сумісний з LLM
Rust
Щоб конвертувати відповідь MCP інструменту у формат, який розуміє LLM, додамо допоміжну функцію, що форматує список інструментів. Додайте цей код у файл main.rs нижче функції main. Він використовуватиметься при запитах до LLM:
async fn format_tools(tools: &ListToolsResult) -> Result<Vec<Value>, Box<dyn Error>> {
let tools_json = serde_json::to_value(tools)?;
let Some(tools_array) = tools_json.get("tools").and_then(|t| t.as_array()) else {
return Ok(vec![]);
};
let formatted_tools = tools_array
.iter()
.filter_map(|tool| {
let name = tool.get("name")?.as_str()?;
let description = tool.get("description")?.as_str()?;
let schema = tool.get("inputSchema")?;
Some(json!({
"type": "function",
"function": {
"name": name,
"description": description,
"parameters": {
"type": "object",
"properties": schema.get("properties").unwrap_or(&json!({})),
"required": schema.get("required").unwrap_or(&json!([]))
}
}
}))
})
.collect();
Ok(formatted_tools)
}
Чудово, тепер готові обробляти запити користувача — приступимо.
-4- Обробка запиту підказки користувача
У цій частині коду ми оброблятимемо запити користувача.
TypeScript
-
Додайте метод для виклику LLM:
async callTools( tool_calls: OpenAI.Chat.Completions.ChatCompletionMessageToolCall[], toolResults: any[] ) { for (const tool_call of tool_calls) { const toolName = tool_call.function.name; const args = tool_call.function.arguments; console.log(`Calling tool ${toolName} with args ${JSON.stringify(args)}`); // 2. Викликати інструмент сервера const toolResult = await this.client.callTool({ name: toolName, arguments: JSON.parse(args), }); console.log("Tool result: ", toolResult); // 3. Щось зробити з результатом // Потрібно зробити } }У наведеному коді ми:
-
Додали метод
callTools. -
Метод приймає відповідь LLM і перевіряє, які інструменти були викликані, якщо такі були:
for (const tool_call of tool_calls) { const toolName = tool_call.function.name; const args = tool_call.function.arguments; console.log(`Calling tool ${toolName} with args ${JSON.stringify(args)}`); // виклик інструменту } -
Викликає інструмент, якщо LLM вказує, що його слід викликати:
// 2. Викликати інструмент сервера const toolResult = await this.client.callTool({ name: toolName, arguments: JSON.parse(args), }); console.log("Tool result: ", toolResult); // 3. Зробити щось з результатом // Потрібно зробити
-
-
Оновіть метод
run, включивши виклики LLM таcallTools:// 1. Створіть повідомлення, які є вхідними даними для LLM const prompt = "What is the sum of 2 and 3?" const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [ { role: "user", content: prompt, }, ]; console.log("Querying LLM: ", messages[0].content); // 2. Виклик LLM let response = this.openai.chat.completions.create({ model: "gpt-4.1-mini", max_tokens: 1000, messages, tools: tools, }); let results: any[] = []; // 3. Перегляньте відповідь LLM, для кожного варіанту перевірте, чи містить він виклики інструментів (await response).choices.map(async (choice: { message: any; }) => { const message = choice.message; if (message.tool_calls) { console.log("Making tool call") await this.callTools(message.tool_calls, results); } });
Чудово, ось повний код:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import OpenAI from "openai";
import { z } from "zod"; // Імпортувати zod для валідації схеми
class MyClient {
private openai: OpenAI;
private client: Client;
constructor(){
this.openai = new OpenAI({
baseURL: "https://models.inference.ai.azure.com", // можливо доведеться змінити на цю URL у майбутньому: https://models.github.ai/inference
apiKey: process.env.GITHUB_TOKEN,
});
this.client = new Client(
{
name: "example-client",
version: "1.0.0"
},
{
capabilities: {
prompts: {},
resources: {},
tools: {}
}
}
);
}
async connectToServer(transport: Transport) {
await this.client.connect(transport);
this.run();
console.error("MCPClient started on stdin/stdout");
}
openAiToolAdapter(tool: {
name: string;
description?: string;
input_schema: any;
}) {
// Створити схему zod на основі input_schema
const schema = z.object(tool.input_schema);
return {
type: "function" as const, // Явно встановити тип "function"
function: {
name: tool.name,
description: tool.description,
parameters: {
type: "object",
properties: tool.input_schema.properties,
required: tool.input_schema.required,
},
},
};
}
async callTools(
tool_calls: OpenAI.Chat.Completions.ChatCompletionMessageToolCall[],
toolResults: any[]
) {
for (const tool_call of tool_calls) {
const toolName = tool_call.function.name;
const args = tool_call.function.arguments;
console.log(`Calling tool ${toolName} with args ${JSON.stringify(args)}`);
// 2. Викликати інструмент сервера
const toolResult = await this.client.callTool({
name: toolName,
arguments: JSON.parse(args),
});
console.log("Tool result: ", toolResult);
// 3. Зробити щось з результатом
// TODO
}
}
async run() {
console.log("Asking server for available tools");
const toolsResult = await this.client.listTools();
const tools = toolsResult.tools.map((tool) => {
return this.openAiToolAdapter({
name: tool.name,
description: tool.description,
input_schema: tool.inputSchema,
});
});
const prompt = "What is the sum of 2 and 3?";
const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
{
role: "user",
content: prompt,
},
];
console.log("Querying LLM: ", messages[0].content);
let response = this.openai.chat.completions.create({
model: "gpt-4.1-mini",
max_tokens: 1000,
messages,
tools: tools,
});
let results: any[] = [];
// 3. Переглянути відповідь LLM, для кожного вибору перевірити чи містить виклики інструментів
(await response).choices.map(async (choice: { message: any; }) => {
const message = choice.message;
if (message.tool_calls) {
console.log("Making tool call")
await this.callTools(message.tool_calls, results);
}
});
}
}
let client = new MyClient();
const transport = new StdioClientTransport({
command: "node",
args: ["./build/index.js"]
});
client.connectToServer(transport);
Python
-
Додамо імпорти, потрібні для виклику LLM:
# великий мовний модуль import os from azure.ai.inference import ChatCompletionsClient from azure.ai.inference.models import SystemMessage, UserMessage from azure.core.credentials import AzureKeyCredential import json -
Потім додамо функцію, що викликатиме LLM:
# llm def call_llm(prompt, functions): token = os.environ["GITHUB_TOKEN"] endpoint = "https://models.inference.ai.azure.com" model_name = "gpt-4o" client = ChatCompletionsClient( endpoint=endpoint, credential=AzureKeyCredential(token), ) print("CALLING LLM") response = client.complete( messages=[ { "role": "system", "content": "You are a helpful assistant.", }, { "role": "user", "content": prompt, }, ], model=model_name, tools = functions, # Необов’язкові параметри temperature=1., max_tokens=1000, top_p=1. ) response_message = response.choices[0].message functions_to_call = [] if response_message.tool_calls: for tool_call in response_message.tool_calls: print("TOOL: ", tool_call) name = tool_call.function.name args = json.loads(tool_call.function.arguments) functions_to_call.append({ "name": name, "args": args }) return functions_to_callУ наведеному коді ми:
- Передали функції, які знайшли на MCP сервері та конвертували, до LLM.
- Викликали LLM з цими функціями.
- Перевірили результат, щоб зрозуміти, які функції слід викликати.
- Передали масив функцій для виклику.
-
Остаточний крок — оновити основний код:
prompt = "Add 2 to 20" # запитайте у LLM, які інструменти взагалі використовувати, якщо такі є functions_to_call = call_llm(prompt, functions) # викликати запропоновані функції for f in functions_to_call: result = await session.call_tool(f["name"], arguments=f["args"]) print("TOOLS result: ", result.content)Ось і фінальна частина, у коді вище ми:
- Викликаємо інструмент MCP через
call_toolза функцією, яку LLM визначив як потрібну для виклику на основі підказки. - Виводимо результат виклику інструменту MCP сервера.
- Викликаємо інструмент MCP через
.NET
-
Покажемо код для запиту підказки LLM:
var tools = await GetMcpTools(); for (int i = 0; i < tools.Count; i++) { var tool = tools[i]; Console.WriteLine($"MCP Tools def: {i}: {tool}"); } // 0. Define the chat history and the user message var userMessage = "add 2 and 4"; chatHistory.Add(new ChatRequestUserMessage(userMessage)); // 1. Define tools ChatCompletionsToolDefinition def = CreateToolDefinition(); // 2. Define options, including the tools var options = new ChatCompletionsOptions(chatHistory) { Model = "gpt-4.1-mini", Tools = { tools[0] } }; // 3. Call the model ChatCompletions? response = await client.CompleteAsync(options); var content = response.Content;У наведеному коді ми:
- Отримали інструменти з MCP сервера —
var tools = await GetMcpTools(). - Визначили підказку користувача
userMessage. - Створили об’єкт опцій з моделлю та інструментами.
- Зробили запит до LLM.
- Отримали інструменти з MCP сервера —
-
Останній крок — перевірка, чи вважає LLM, що потрібно викликати функцію:
// 4. Check if the response contains a function call ChatCompletionsToolCall? calls = response.ToolCalls.FirstOrDefault(); for (int i = 0; i < response.ToolCalls.Count; i++) { var call = response.ToolCalls[i]; Console.WriteLine($"Tool call {i}: {call.Name} with arguments {call.Arguments}"); //Tool call 0: add with arguments {"a":2,"b":4} var dict = JsonSerializer.Deserialize<Dictionary<string, object>>(call.Arguments); var result = await mcpClient.CallToolAsync( call.Name, dict!, cancellationToken: CancellationToken.None ); Console.WriteLine(result.Content.First(c => c.Type == "text").Text); }У коді ми:
- Пройшлися циклом по списку викликів функцій.
- Для кожного виклику парсимо назву та аргументи і викликаємо інструмент на MCP сервері через клієнта MCP. Потім виводимо результат.
Ось код повністю:
using Azure;
using Azure.AI.Inference;
using Azure.Identity;
using System.Text.Json;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
var endpoint = "https://models.inference.ai.azure.com";
var token = Environment.GetEnvironmentVariable("GITHUB_TOKEN"); // Your GitHub Access Token
var client = new ChatCompletionsClient(new Uri(endpoint), new AzureKeyCredential(token));
var chatHistory = new List<ChatRequestMessage>
{
new ChatRequestSystemMessage("You are a helpful assistant that knows about AI")
};
var clientTransport = new StdioClientTransport(new()
{
Name = "Demo Server",
Command = "/workspaces/mcp-for-beginners/03-GettingStarted/02-client/solution/server/bin/Debug/net8.0/server",
Arguments = [],
});
Console.WriteLine("Setting up stdio transport");
await using var mcpClient = await McpClient.CreateAsync(clientTransport);
ChatCompletionsToolDefinition ConvertFrom(string name, string description, JsonElement jsonElement)
{
// convert the tool to a function definition
FunctionDefinition functionDefinition = new FunctionDefinition(name)
{
Description = description,
Parameters = BinaryData.FromObjectAsJson(new
{
Type = "object",
Properties = jsonElement
},
new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })
};
// create a tool definition
ChatCompletionsToolDefinition toolDefinition = new ChatCompletionsToolDefinition(functionDefinition);
return toolDefinition;
}
async Task<List<ChatCompletionsToolDefinition>> GetMcpTools()
{
Console.WriteLine("Listing tools");
var tools = await mcpClient.ListToolsAsync();
List<ChatCompletionsToolDefinition> toolDefinitions = new List<ChatCompletionsToolDefinition>();
foreach (var tool in tools)
{
Console.WriteLine($"Connected to server with tools: {tool.Name}");
Console.WriteLine($"Tool description: {tool.Description}");
Console.WriteLine($"Tool parameters: {tool.JsonSchema}");
JsonElement propertiesElement;
tool.JsonSchema.TryGetProperty("properties", out propertiesElement);
var def = ConvertFrom(tool.Name, tool.Description, propertiesElement);
Console.WriteLine($"Tool definition: {def}");
toolDefinitions.Add(def);
Console.WriteLine($"Properties: {propertiesElement}");
}
return toolDefinitions;
}
// 1. List tools on mcp server
var tools = await GetMcpTools();
for (int i = 0; i < tools.Count; i++)
{
var tool = tools[i];
Console.WriteLine($"MCP Tools def: {i}: {tool}");
}
// 2. Define the chat history and the user message
var userMessage = "add 2 and 4";
chatHistory.Add(new ChatRequestUserMessage(userMessage));
// 3. Define options, including the tools
var options = new ChatCompletionsOptions(chatHistory)
{
Model = "gpt-4.1-mini",
Tools = { tools[0] }
};
// 4. Call the model
ChatCompletions? response = await client.CompleteAsync(options);
var content = response.Content;
// 5. Check if the response contains a function call
ChatCompletionsToolCall? calls = response.ToolCalls.FirstOrDefault();
for (int i = 0; i < response.ToolCalls.Count; i++)
{
var call = response.ToolCalls[i];
Console.WriteLine($"Tool call {i}: {call.Name} with arguments {call.Arguments}");
//Tool call 0: add with arguments {"a":2,"b":4}
var dict = JsonSerializer.Deserialize<Dictionary<string, object>>(call.Arguments);
var result = await mcpClient.CallToolAsync(
call.Name,
dict!,
cancellationToken: CancellationToken.None
);
Console.WriteLine(result.Content.OfType<TextContentBlock>().First().Text);
}
// 6. Print the generic response
Console.WriteLine($"Assistant response: {content}");
Java
try {
// Виконуйте запити природною мовою, які автоматично використовують інструменти MCP
String response = bot.chat("Calculate the sum of 24.5 and 17.3 using the calculator service");
System.out.println(response);
response = bot.chat("What's the square root of 144?");
System.out.println(response);
response = bot.chat("Show me the help for the calculator service");
System.out.println(response);
} finally {
mcpClient.close();
}
У наведеному коді ми:
- Використали прості підказки природною мовою для взаємодії з інструментами MCP сервера
- Фреймворк LangChain4j автоматично виконує:
- Перетворення підказок користувача на виклики інструментів, коли це потрібно
- Виклик відповідних MCP інструментів на основі рішення LLM
- Управління потоком розмови між LLM та MCP сервером
- Метод
bot.chat()повертає відповіді природною мовою, що можуть містити результати виконання MCP інструментів - Такий підхід забезпечує безшовний користувацький досвід, де користувачам не потрібно знати про внутрішню реалізацію MCP
Повний приклад коду:
public class LangChain4jClient {
public static void main(String[] args) throws Exception { ChatLanguageModel model = OpenAiOfficialChatModel.builder()
.isGitHubModels(true)
.apiKey(System.getenv("GITHUB_TOKEN"))
.timeout(Duration.ofSeconds(60))
.modelName("gpt-4.1-nano")
.timeout(Duration.ofSeconds(60))
.build();
McpTransport transport = new HttpMcpTransport.Builder()
.sseUrl("http://localhost:8080/sse")
.timeout(Duration.ofSeconds(60))
.logRequests(true)
.logResponses(true)
.build();
McpClient mcpClient = new DefaultMcpClient.Builder()
.transport(transport)
.build();
ToolProvider toolProvider = McpToolProvider.builder()
.mcpClients(List.of(mcpClient))
.build();
Bot bot = AiServices.builder(Bot.class)
.chatLanguageModel(model)
.toolProvider(toolProvider)
.build();
try {
String response = bot.chat("Calculate the sum of 24.5 and 17.3 using the calculator service");
System.out.println(response);
response = bot.chat("What's the square root of 144?");
System.out.println(response);
response = bot.chat("Show me the help for the calculator service");
System.out.println(response);
} finally {
mcpClient.close();
}
}
}
Rust
Тут відбувається більшість роботи. Ми викличемо LLM з початковою підказкою користувача, потім обробимо відповідь, щоб з’ясувати, чи потрібно викликати якісь інструменти. Якщо так, викликаємо ці інструменти і продовжуємо розмову з LLM, доки більше не буде викликів інструментів і не отримаємо остаточну відповідь.
Оскільки викликів LLM буде кілька, визначимо функцію для їх обробки. Додайте цю функцію у ваш файл main.rs:
async fn call_llm(
client: &Client<OpenAIConfig>,
messages: &[Value],
tools: &ListToolsResult,
) -> Result<Value, Box<dyn Error>> {
let response = client
.completions()
.create_byot(json!({
"messages": messages,
"model": "openai/gpt-4.1",
"tools": format_tools(tools).await?,
}))
.await?;
Ok(response)
}
Функція приймає клієнта LLM, список повідомлень (включно з підказкою користувача), інструменти від MCP сервера і надсилає запит до LLM, повертаючи відповідь.
Відповідь від LLM буде містити масив choices. Нам потрібно обробити результат, щоб перевірити, чи є присутні tool_calls. Це дає нам знати, що LLM запитує виклик певного інструменту з аргументами. Додайте наступний код в кінець вашого файлу main.rs, щоб визначити функцію для обробки відповіді LLM:
async fn process_llm_response(
llm_response: &Value,
mcp_client: &RunningService<RoleClient, ()>,
openai_client: &Client<OpenAIConfig>,
mcp_tools: &ListToolsResult,
messages: &mut Vec<Value>,
) -> Result<(), Box<dyn Error>> {
let Some(message) = llm_response
.get("choices")
.and_then(|c| c.as_array())
.and_then(|choices| choices.first())
.and_then(|choice| choice.get("message"))
else {
return Ok(());
};
// Друкувати вміст, якщо доступний
if let Some(content) = message.get("content").and_then(|c| c.as_str()) {
println!("🤖 {}", content);
}
// Обробка викликів інструментів
if let Some(tool_calls) = message.get("tool_calls").and_then(|tc| tc.as_array()) {
messages.push(message.clone()); // Додати повідомлення асистента
// Виконати кожен виклик інструменту
for tool_call in tool_calls {
let (tool_id, name, args) = extract_tool_call_info(tool_call)?;
println!("⚡ Calling tool: {}", name);
let result = mcp_client
.call_tool(CallToolRequestParam {
name: name.into(),
arguments: serde_json::from_str::<Value>(&args)?.as_object().cloned(),
})
.await?;
// Додати результат інструменту до повідомлень
messages.push(json!({
"role": "tool",
"tool_call_id": tool_id,
"content": serde_json::to_string_pretty(&result)?
}));
}
// Продовжити розмову з результатами інструменту
let response = call_llm(openai_client, messages, mcp_tools).await?;
Box::pin(process_llm_response(
&response,
mcp_client,
openai_client,
mcp_tools,
messages,
))
.await?;
}
Ok(())
}
Якщо присутні tool_calls, вона витягує інформацію про інструмент, звертається до MCP сервера із запитом інструменту та додає результати до повідомлень розмови. Потім розмова продовжується з LLM, а повідомлення оновлюються відповіддю асистента та результатами виклику інструменту.
Щоб витягнути інформацію про виклик інструменту, який LLM повертає для викликів MCP, ми додамо ще одну допоміжну функцію, щоб вилучити все необхідне для здійснення виклику. Додайте наступний код в кінець вашого файлу main.rs:
fn extract_tool_call_info(tool_call: &Value) -> Result<(String, String, String), Box<dyn Error>> {
let tool_id = tool_call
.get("id")
.and_then(|id| id.as_str())
.unwrap_or("")
.to_string();
let function = tool_call.get("function").ok_or("Missing function")?;
let name = function
.get("name")
.and_then(|n| n.as_str())
.unwrap_or("")
.to_string();
let args = function
.get("arguments")
.and_then(|a| a.as_str())
.unwrap_or("{}")
.to_string();
Ok((tool_id, name, args))
}
З усіма компонентами на місці, ми тепер можемо обробляти початковий запит користувача та звертатися до LLM. Оновіть вашу функцію main, додаючи наступний код:
// Розмова LLM з викликами інструментів
let response = call_llm(&openai_client, &messages, &tools).await?;
process_llm_response(
&response,
&mcp_client,
&openai_client,
&tools,
&mut messages,
)
.await?;
Це звернеться до LLM із початковим промптом користувача, який запитує суму двох чисел, та обробить відповідь для динамічної обробки викликів інструментів.
Чудово, ви це зробили!
Завдання
Візьміть код із вправи і розширте сервер, додавши ще кілька інструментів. Потім створіть клієнта з LLM, як у вправі, і протестуйте його з різними промптом, щоб переконатися, що всі ваші серверні інструменти викликаються динамічно. Такий спосіб побудови клієнта забезпечує відмінний користувацький досвід, оскільки користувачі можуть використовувати промпти замість точних команд клієнта та не помічати виклики MCP сервера.
Розв’язок
Основні висновки
- Додавання LLM до вашого клієнта забезпечує кращий спосіб взаємодії користувачів із MCP серверами.
- Потрібно конвертувати відповідь MCP сервера в те, що LLM зможе зрозуміти.
Приклади
- Калькулятор на Java
- Калькулятор на .Net
- Калькулятор на JavaScript
- Калькулятор на TypeScript
- Калькулятор на Python
- Калькулятор на Rust
Додаткові ресурси
Що далі
Відмова від відповідальності: Цей документ було перекладено за допомогою сервісу штучного інтелекту для перекладу Co-op Translator. Хоча ми прагнемо до точності, будь ласка, майте на увазі, що автоматичні переклади можуть містити помилки або неточності. Оригінальний документ рідною мовою слід вважати авторитетним джерелом. Для критично важливої інформації рекомендується професійний людський переклад. Ми не несемо відповідальності за будь-які непорозуміння або неправильні тлумачення, що виникли внаслідок використання цього перекладу.