Главная / Статьи

FastMCP Voice Stack Часть 1: инструменты, Stdio/HTTP, STT и TTS

Часть 1 голосового агента: сервер инструментов MCP с FastMCP через stdio и HTTP, вспомогательные компоненты для получения информации о погоде и видео, а также подключенные функции преобразования речи в текст с помощью микрофона и генерации речи из текста.

Издатель: REACTAPP.TOP15 мин чтения3461 слов

Источники: **Part 1: VOICE ASSISTANT, FASTMCP, LANGRAPH(J.A.V.I.S)🤖**

Создание голосового агента с использованием инструментов

В этом руководстве рассматриваются серверы MCP, преобразование речи в текст и текста в речь, а также путь реализации проекта, при котором агент слышит голос, вызывает инструменты через MCP и воспроизводит полученные результаты. «LLM» означает большую языковую модель, отвечающую за принятие решений.

FastMCP и протокол Model Context Protocol

MCP — это открытый протокол, предоставляющий моделям доступ к инструментам, ресурсам и запросам. Сервер MCP перечисляет доступные инструменты; клиенты (агенты LangChain, Claude Desktop, Cursor и подобные) подключаются к нему, позволяя модели использовать эти инструменты.

FastMCP — это фреймворк на Python для серверов MCP, аналогичный Flask для веб-приложений или FastAPI для HTTP-API. Чтобы опубликовать обычные функции, их нужно задекорировать атрибутом @mcp.tool.

Способы передачи данных

Стандартный ввод-вывод. Клиент запускает server.py в качестве подпроцесса и передает данные в формате JSON через stdin/stdout. Сетевое взаимодействие отсутствует. Идеально подходит для инструментов, работающих исключительно локально вместе с процессом агента.

HTTP. Сервер прослушивает определенный хост и порт (например, 127.0.0.1:8000) и работает независимо. Локальные или удаленные клиенты, способные обратиться по URL, могут вызывать эти инструменты. Для обеспечения доступа к серверу, расположенному на ноутбуке, часто используется туннель вроде Ngrok.

Минимальный сервер и клиент с поддержкой стандартного ввода-вывода

Настройте среду с помощью uv, затем создайте небольшой сервер для стандартного ввода-вывода:

On windows(Powershell):
PS>> irm https://astral.sh/uv/install.ps1 | iex
Linux:
linux> curl -LsSf https://astral.sh/uv/install.sh | sh
Verify Installation:
 Ps>> uv --version
Linux:
 linux> uv --version

Create a virtual environment:
Ps>> uv venv
Linux:
 linux> uv venv
Now activate our virtual environment:
 Ps>>  “.venv/Scripts/activate”
Linux:
 linux> source .venv/bin/activate

To install packages using uv we do:
Ps>> uv pip install <package_name>

- In our case we need fastMCP library:
Ps>> uv pip install fastmcp
#!/usr/bin/python3
from fastmcp import FastMCP     # our library
from datetime import datetime  # used in our second tool

# create an object from the library
mcp=FastMCP("Demo🚀🚀")

# Python decorator--> from the FastMCP library
# The "@" decorator below exposes the function as a tool
@mcp.tool
def calculate(a:int, b:int)-> int:
    """
    Add two Numbers.
    """
    # the above is a Python DocString--> AI reads it to know when to use the tool

    return a+b

# lets add another tool
@mcp.tool
def current_time()-> str:
    """
    Returns the current local date and time
    """

    # call the datetime library to get this
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

if __name__=="__main__":
    # By default the mode of transport here is "stdio"
    mcp.run()

Ключевые моменты: @mcp.tool регистрирует функцию; документация указывает модели, когда ее вызывать; mcp.run() по умолчанию использует стандартный ввод-вывод.

Клиент без ЯЗЫКА больших моделей может напрямую перечислять и вызывать инструменты:

#!/usr/bin/python3
import asyncio
from fastmcp import Client

# our function will be an asynchronous function:
# MCP transport is I/O bound and the async library lets our program
# wait for tool results without freezing the whole program
# Async library also allows the AI to run multiple tools concurrently
async def main():
    # we get the server name we have for our server and pass it below
    async with Client("mcp_server.py") as client:
        # Now list available tools
        tools=await client.list_tools()
        print(f"Tools: {[t.name for t in tools]}")

        # call the add tool
        result=await client.call_tool("add", {"a":3, "b":4})
        print(f"\nTool Result: {result}")

        # call the second tool(remember, it had no arguments)
        result=await client.call_tool("current_time")
        print(f"\nCurrent time result: {result}")

# run our function
asyncio.run(main())

"""
========Exclude this: Its my tool result when I run on cmd/terminal======
example_2>python mcp_client.py
Tools: ['add', 'current_time']

Tool Result: CallToolResult(content=[TextContent(type='text', text='7', annotations=None, meta=None)], structured_content={'result': 7}, meta={'fastmcp': {'wrap_result': True}}, data=7, is_error=False)

Current time result: CallToolResult(content=[TextContent(type='text', text='2026-08-23 19:31:48', annotations=None, meta=None)], structured_content={'result': '2026-08-23 19:31:48'}, meta={'fastmcp': {'wrap_result': True}}, data='2026-08-23 19:31:48', is_error=False)

"""

Вариант с HTTP

Та же самая поверхность инструментов, но разный режим работы:

#!/usr/bin/python3
from fastmcp import FastMCP     # our library
from datetime import datetime  # used in our second tool

# create an object from the library
mcp=FastMCP("Demo🚀🚀")

# Python decorator--> from the FastMCP library
# The "@" decorator below exposes the function as a tool
@mcp.tool
def add(a:int, b:int)-> int:
    """
    Add two Numbers.
    """
    # the above is a Python DocString--> AI reads it to know when to use the tool

    return a+b

# lets add another tool
@mcp.tool
def current_time()-> str:
    """
    Returns the current local date and time
    """

    # call the datetime library to get this
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

if __name__=="__main__":
    # We now change the mode of transport to http from the default stdio
    mcp.run(transport="http", host="127.0.0.1", port=8000)
# We now change the mode of transport to http from the default stdio
    mcp.run(transport="http", host="127.0.0.1", port=8000)

Сервер объявляет что-то вроде http://127.0.0.1:8000/mcp. Клиент указывает на этот URL:

#!/usr/bin/python3
import asyncio
from fastmcp import Client

# our function will be an asynchronous function:
# MCP transport is I/O bound and the async library lets our program
# wait for tool results without freezing the whole program
# Async library also allows the AI to run multiple tools concurrently
async def main():
    # we pass the localIP:port url that our server is listening on
    async with Client("http://127.0.0.1:8000/mcp") as client:
        # Now list available tools
        tools=await client.list_tools()
        # iterate through the list of tools and display only there names
        print(f"Tools: {[t.name for t in tools]}")

        # call the add tool
        result=await client.call_tool("add", {"a":3, "b":4})
        print(f"\nTool Result: {result}")

        # call the second tool(remember, it had no arguments)
        result=await client.call_tool("current_time")
        print(f"\nCurrent time result: {result}")

# run our function
asyncio.run(main())

"""
======Exclude this: Its my tool result when I run on cmd/terminal=======
example_2>python mcp_client.py
Tools: ['add', 'current_time']

Tool Result: CallToolResult(content=[TextContent(type='text', text='7', annotations=None, meta=None)], structured_content={'result': 7}, meta={'fastmcp': {'wrap_result': True}}, data=7, is_error=False)

Current time result: CallToolResult(content=[TextContent(type='text', text='2026-08-23 19:54:50', annotations=None, meta=None)], structured_content={'result': '2026-08-23 19:54:50'}, meta={'fastmcp': {'wrap_result': True}}, data='2026-08-23 19:54:50', is_error=False)

"""
# we pass the localIP:port url that our server is listening on
    async with Client("http://127.0.0.1:8000/mcp") as client:

Результаты совпадают со стандартным вводом-выводом; изменяется только топология обработки.

Почему FastMCP помогает агентам

Модель видит название, описание, параметры и формат возвращаемых данных — а не внутренний код. Механизмы коммуникации остаются в рамках фреймворка. Один и тот же сервер может находиться на удаленном хосте и обслуживать множество клиентов, в отличие от инструментов, закодированных исключительно внутри одного процесса LangChain.

Сервер проекта MCP для голосового помощника

Планируемые к использованию инструменты для помощника: калькулятор, прогноз погоды, веб-поиск Tavily, сбор данных Firecrawl в формат markdown/JSON, обработка изображений и работа с датами/временем. Для локальной работы используется стандартный ввод-вывод.

Создайте проект с файлом .env для хранения ключей и файлом .gitignore, который будет исключать .env. Установите необходимые компоненты с помощью uv:

cmd>> uv pip install requests fastmcp simpleeval python-dotenv tavily-python firecrawl-py langchain_groq langchain_core langchain

Более сложные вспомогательные функции разместите в пакете tools:

cmd>> mkdir tools

Вспомогательная функция для получения информации о погоде

Получите ключ от OpenWeather, сохраните его в .env и реализуйте функцию поиска информации о погоде:

#!/usr/bin/python3
import requests
import os
from dotenv import load_dotenv

# Load our API key from environment variables
load_dotenv()

# access it using the os library
API_KEY=os.getenv("WEB_API")

def get_weather(city):
    # fetching weather from open weather web app
    url=f"https://api.openweathermap.org/data/2.5/weather?q={city}&appid={API_KEY}&units=metric"

    # Send our HTTP request
    response=requests.get(url)

    # filter and iterate through the request
    if response.status_code==200:
        # If we connected to the web seaver. Convert the response received to json
        data=response.json()

        temperature=data["main"]["temp"]
        humidity=data["main"]["humidity"]
        description=data["weather"][0]["description"]
        wind_speed=data["wind"]["speed"]
        weather_report = f"""
    Current Weather Report

    Location: {city}
    Temperature: {temperature}°C
    Humidity: {humidity}%
    Conditions: {description}
    Wind Speed: {wind_speed} m/s
    """
        # return the above report
        return weather_report

    else:
        return f"Error retrieving weather data: {response.status_code}"

Вспомогательная функция для обработки изображений

Кодируйте изображения в формате base64 и отправляйте их через LangChain в модель Groq, способную обрабатывать изображения (например, модель Qwen vision/reasoning). Храните ключ Groq в .env.

#!/usr/bin/python3
from langchain_groq import ChatGroq
from dotenv import load_dotenv
import os
import base64
from langchain_core.messages import HumanMessage

# load environment variables--> API keys from the ".env" file
load_dotenv()

# the llm setup---> take note of the model_name that has vision
api_key=os.getenv("GROQ_API_KEY")

llm=ChatGroq(
    temperature=0.3,
    groq_api_key=os.getenv("GROQ_API_KEY"),
    model_name="qwen/qwen3.6-27b"
)

# first step is to convert the image to base64
# We will send it to the API in this format
def encode_image(image):
    # open the image in binary and encode it
    with open(image, "rb") as f:
        return base64.b64encode(f.read()).decode()

# Call the function to send encoded image data to LLM
def describe_image(image):
    # get encoded image data
    encoded_image=encode_image(image)

    # Prepare prompt
    message= HumanMessage(
        content=[
            {
                "type":"text",
                "text":"Describe this Image. Be brief"
            },
            {
                "type":"image_url",
                # fetching image locally. If not local use: 'url':'https:<url_for_iamge>'
                "image_url":{
                    "url":f"data:image/png;base64,{encoded_image}"
                }
            }
        ]
    )

    # call our LLM passing the above prompt with our encoded image data
    result=llm.invoke([message])

    # get the content part. Thats what we care about to be returned
    result=result.content

    # Remove reasoning section--> we want to reduce the amount of text returned--> reducing token consumption
    if "</think>" in result:
        result = result.split("</think>", 1)[1].strip()

    return result


# --------Delete this part below after testing and save only the function above-----------
print("[+] Starting description......")
# Make sure image is inthe same directory as vision.py . If not use the filepath
response=describe_image("my_quiz.png")

print(f"\nImage Data: {response}")

"""
=================This is the result I got=======================
cmd>> python vision.py
[+] Starting description......

Image Data: The image displays a text-based "Python Quick Quiz" containing three questions. The text is shown twice: once at the top and again in a zoomed-in view below. The questions are:

1.  Define Python in one line
2.  Difference between variable and constant in one line
3.  Write code to get input from the user and add the two numbers

"""
cmd>> cd ..

# make a file mcp_server.py if using vs code
# Now the shell should look like below
cmd>> ls
tools mcp_server.py .env

Сборка файла mcp_server.py

Добавьте ключи Firecrawl и Tavily, импортируйте вспомогательные функции и зарегистрируйте остальные инструменты:

#!/usr/bin/python3
from datetime import datetime
from fastmcp import FastMCP
from simpleeval import simple_eval
import os
from dotenv import load_dotenv
from tavily import TavilyClient
from firecrawl import FirecrawlApp
from typing import Literal

# user defined function here. Located in the tools directory
from tools.vision import describe_image
from tools.weather import get_weather

# ----------Loads APIs Accessible to our Program----------
#loads environment variables. Our APIs in .env file to be accessible here
load_dotenv()

# we get the Tavily API here--> use os library and save to a variable
tavily_client=TavilyClient(api_key=os.getenv("TAVILLY_API")) # Tavily

# firecrawl setup. Get API key using OS library and save to a variable
app=FirecrawlApp(api_key=os.getenv("FIRECRAWL"))
#---------------------------------------------------------

# create an MCP object from the class
mcp=FastMCP("local-tools")

#-----1. Time tool
# the decorator turns our function into a tool
@mcp.tool
def current_time()-> str:
    """
    Return the current local date and time
    Use this when the user asks what time or date it is
    """
    # Docstring provides tool description to the LLM
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

#----2: Calculator to solve expressions
#Next tool will be a calculator. Can solve complex maths equations
@mcp.tool
def calculate(expression: str)-> str:
    """
    Safely evaluate a mathematical expression
    Returns a string suitable for an LLM
    """
    try:
        # pass the expression from an LLM here
        result=simple_eval(expression)
        return f"""
Calculation

Expression: {expression}
Result: {result}
"""
    except Exception as e:
        return f"Calculation Error: {e}"

#----3. Vision into images
# ability to describe images. We wrote the code in the tools directory "vision.py"
@mcp.tool
def image_info(image_path: str)-> str:
    """
    Use this when user wants to know what the image entails
    User must have provided the path to where the image is
    The image can be on the web via URL or on local machine
    Returns the description of the image content
    """
    # call the function that will carry this task
    try:
        # call our "vision.py" function we have imported from tools
        result=describe_image(image_path)
        # return the result gotten from the model
        return result

    except Exception as e:
        return f"Image Description Error: {e}"

# ----4. Tavily Search Tool
@mcp.tool
def internet_search(
    query: str,
    max_results: int=5,
    topic: Literal["general", "news", "finance"] = "general", # Literal forces LLM to choose 1 from the 3 in the List
    include_raw_content:bool=False,
):
    """Run a web search"""
    try:
        return tavily_client.search(
            query,
            max_results=max_results,
            include_raw_content=include_raw_content,
            topic=topic,
        )
    except Exception as e:
        return f"Web Search Error: {e}"

# ----5. Scrape Webpage and return result in markdown
@mcp.tool
def scrape_webpage(url: str)-> str:
    """
    Read a webpage and return clean markdown suitable for an LLM
    """
    # Send our request and store the markdown data
    response=app.scrape_url(
        url=url,
        formats=["markdown"]
    )

    markdown=getattr(response, "markdown", "")

    # if the response is empty
    if not markdown:
        return "Unable to extract content from the webpage."

    return f"""
Source URL:
{url}

Extracted Content:
{markdown}
"""

# -----6. Weather forecast
@mcp.tool
def weather_forecast(city: str)-> str:
    """
    Use this tool when you have the city/Town name you want to look
    up for the current weather conditions
    """
    # call our function
    try:
        result=get_weather(city)
        return result
    except Exception as e:
        return f"Weather tool Error: {e}"

if __name__=="__main__":
    # run the MCP server over stdio
    mcp.run()

Упражнение: запустите сервер с помощью ручного клиента без использования LLM, затем попробуйте работу в режиме HTTP через Cursor или Claude Code для дополнительной практики.

Ввод речи и вывод речи

Функция преобразования речи в текст превращает аудиосигнал с микрофона в текстовые строки. Одним из способов является облачное решение Google STT; Whisper и Vosk работают офлайн — для более высокой скорости рекомендуется использовать Whisper или Vosk на компьютерах с процессором без графики.

Варианты синтеза речи: pyttsx3 (локальный, «роботизированный» голос, бесплатно), Piper (локальная нейронная сеть, поддержка многих языков), Google TTS (облачный сервис, часто работает с файлами), а также платные API вроде ElevenLabs.

Небольшая демонстрация включает захват аудио с микрофона с использованием сервиса распознавания Google, выполнение простой операции и воспроизведение речи с помощью pyttsx3:

cmd>> uv pip install speech_recognition pyttsx3
#!/usr/bin/python3

# Function processes the user's spoken command as transcribed by Google text to speech
# returns appropriate response
def process_command(command):
    # Checked if the user asked about the weather
    command=command.lower()   # convert the user input to lower case

    if "weather" in command:
        return "Sir, no storm today. Wear a Tshirt!"

    # check if the user requested a joke
    elif "joke" in command:
        return "Why did the Computer go to therapy? Because it had too many bytes"

    # check if the user mentions wikipedia
    elif "wikipedia" in command:
        return "Sir we have not implemented that, sorry!"

    else:
        return "Sorry Sir, I didn't understand that"

# We are not using an LLM yet
#!/usr/bin/python3
import speech_recognition as sr
import pyttsx3
# our user defined library actions.py
from actions import process_action

# -----Step 1: Listening your Voice(Speech-to-text)-----
# > Function records audio from the microphone
# > Converts it into text using Google's speech recognition API
def listen():
    # create a speech recognizer object
    recognizer=sr.Recognizer()

    # open the default microphone--> get our voice input
    with sr.Microphone() as source:
        # Print on terminal recording has started
        print("\n\n🎤Listening.....")

        # listen until the user stops speaking
        # recorded audio is stored in a variable 'audio'
        audio=recognizer.listen(source)

    # -Since interacting with an API better to use exception handling. Prevents program from crashing
    try:
        # send the recorded audio to Google's Speech Recognition services
        # transcribe our input into text
        text=recognizer.recognize_google(audio)

        # Display what has been transcribed
        print(f"You Said: {text}")

        # return text in lower case. Easier to compare it later
        return text.lower() #--> now we want to pass it to actions.py

    except sr.UnknownValueError:
        # if empty whatever was transcribed
        return ""

    except sr.RequestError:
        # incase no internet or googles speech API is unreachable
        return "\nError: Check Internet Connection"


# ------Step 2: Talking Back using pyttsx(Text-to-Speech)
def speak(text):
    # display what is to be spoken
    print(f"JARVIS🤖: {text}")

    # To avoid error we create a freshh engine instance every time
    engine=pyttsx3.init()
    # speed of the audio
    engine.setProperty('rate', 170)
    # the volume
    engine.setProperty('volume', 1)

    # execute our text to be spoken now
    engine.say(text)
    engine.runAndWait()
    engine.stop()    # clean up after speaking since we are creating a new instance each time


#------Step 3: Execute Everything---------
def main():
    speak("Hello Sir, I am JARVIS. How can I help you today? ")

    # Create an infinity loop for user interaction
    while True:
        # get the input voice and convert to text using the listen function(Has our microphone, Google text-to-speech model)
        command=listen()

        # Check if command is empty we continue--> restarts execution of the infinity loop and skips the other code
        if command=="":
            continue

        # if the command has exit or quit terminate the program
        elif "exit" in command or "quit" in command:
            speak("Goodbye Sir!")
            break

        # Now pass our command transcribed to our action.py. Process_action command will execute and return results
        response=process_action(command)

        #Output the results from our user defined function
        speak(response)

# call main function
if __name__=="__main__":
    main()

"""
=====================Do Not Add this Part: My results on CMD=======
for_blog>python main.py
JARVIS🤖: Hello Sir, I am JARVIS. How can I help you today?

🎤Listening.....
You Said: what is the weather today
JARVIS🤖: Sir, no storm today. Wear a Tshirt!


🎤Listening.....
You Said: do you do you have Wikipedia
JARVIS🤖: Sir we have not implemented that, sorry!

🎤Listening.....
You Said: tell me a joke
JARVIS🤖: Why did the Computer go to therapy? Because it had too many bytes


🎤Listening.....
You Said: exit
JARVIS🤖: Goodbye Sir!

"""
cmd>> python main.py

Заключение первой части

В первой части рассматривались инструменты MCP и базовый цикл ввода-вывода голоса. Во второй части будут использоваться LangChain и LangGraph, что позволит агенту координировать действия этих инструментов с учётом памяти и маршрутизации.

Усиление безопасности стека MCP и обработки голоса перед второй частью

Локальные демо-версии скрывают способы сбоев, которые проявляются сразу при подключении второго клиента. При использовании протокола HTTP необходимо как можно раньше провести аутентификацию — даже с использованием общего токена-носителя, — чтобы публичный туннель не превратился в инструмент с полными правами. Фиксируйте название инструмента, время задержки и тип ошибки; при голосовых сессиях трудно заметить, что инструмент для анализа погоды теряет связь на тридцать секунд, пока пользователь ждет передачи речи.

Правила конфиденциальности становятся ещё более важными, когда к системе добавляются ключи видения и поиска. Избегайте включения файла .env в скриншоты и записи. Регулярно меняйте ключи, используемые в учебных материалах. Применяйте принцип минимальных привилегий: ключ Firecrawl, позволяющий собирать данные только с указанных доменов, лучше, чем токен с полными правами в студенческом репозитории.

Что касается обработки голоса, необходимо на каждом этапе решить, использовать локальные решения или облачные. Многие разработчики применяют облачные сервисы распознавания речи для повышения точности и локальные инструменты генерации речи, чтобы избежать передачи ответов в обоих направлениях. Следует задокументировать последствия с точки зрения конфиденциальности: аудио, покидающее устройство, представляет собой другую модель угроз по сравнению с текстовыми инструментами, работающими через стандартные ввод-выводы. Во второй части эти инструменты будут объединены в агента; задача первой части — сделать каждый инструмент абсолютно надежным по отдельности, чтобы ошибки оркестрации не принимались за ошибки MCP.

Дисциплина каталога инструментов

По мере накопления инструментов для вычислений, прогноза погоды, поиска, парсинга, обработки изображений и отображения времени необходимо публиковать одностраничный каталог: название, краткое описание в документации, требуемые переменные окружения, ожидаемая задержка и информация о том, безопасно ли использовать данный инструмент через HTTP. Это приносит пользу как агентам, так и людям. Во время ручных тестов клиента MCP следует проверять не только корректные сценарии работы, но и некорректные аргументы или пустые ключи API, чтобы сбои были заметны на этапе разработки. Для демонстраций речевого ввода необходимо запрограммировать фиксированный список фраз («сколько будет два плюс два», «погода в Найроби», «подготовьте краткое резюме этой страницы») и записать ожидаемую последовательность действий инструментов. Регрессионные тесты становятся возможными только тогда, когда речевой ввод рассматривается как тест интеграции с API, использующий микрофон. Именно такая подготовка позволяет решить задачу настройки LangGraph во второй части как проблему композиции, а не марафон по отладке.

Выбор локального механизма передачи данных для голосового помощника

Stdio обеспечивает приватность сервера MCP только для процесса агента и соответствует демонстрации на ноутбуке, при которой ничто не должно быть доступно из локальной сети. Переходите на HTTP только тогда, когда второй клиент (Cursor, Claude Code, компьютер коллеги) должен использовать общие инструменты, и даже в этом случае подключайтесь к localhost до тех пор, пока не будет создан обратный прокси с аутентификацией. Смешивание способов передачи данных во время обучения без повторного описания модели угрозы вводит читателей в заблуждение, заставляя их широко раскрывать функции инструментов видения и поиска. Указывайте причину выбора каждый раз, когда меняется точка входа сервера, и оставляйте путь stdio в качестве стандартного для проекта голосового ассистента до тех пор, пока агент во второй части не станет стабильным.

Перед завершением первой части снова запустите сервер MCP в обоих режимах передачи данных и вставьте список инструментов в файл README репозитория. Будущий вы, который будет настраивать LangGraph, поблагодарит нынешнего вас за то, что вы не будете заново искать документацию, которая на самом деле необходима модели для выбора Firecrawl вместо Tavily.

Также проверьте разрешения на использование микрофона и вывод звука через динамик в каждой операционной системе, которую вы демонстрируете; многие «ошибки TTS» возникают из-за выключенных устройств. Имейте запасной вариант, который будет выводить текст ассистента при сбое аудио, чтобы семинар мог продолжаться.

Логистика семинара, спасающая ситуацию

Распечатайте для участников одностраничный справочник: как запустить сервер stdio, как работать с клиентским приложением, какие параметры .env необходимы для получения информации о погоде и видении, а также как должен выглядеть полный список инструментов. Если у кого-то возникают проблемы, спросите, происходит сбой до использования инструментов (микрофон или STT), внутри инструментов (ключи MCP или API) или после их использования (TTS). Такая диагностика уже сокращает время отладки вдвое и позволяет продолжить семинар.