Skip to main content
Glama
whmi1

trainee-mcp-server

by whmi1
README.md
# Test Task: **Trainee Mcp Server**

Учебный MCP-сервер на TypeScript: даёт Claude два инструмента (`add`, `get_weather`) и один ресурс (`favorite-cities`). Работает локально, общается с хостом по транспорту **stdio**, подключается к **Claude Desktop**.

Источник данных о погоде — [Open-Meteo](https://open-meteo.com/). **Ключ API не нужен**, регистрация не требуется.

---

## **Что умеет**

| Тип | Имя | Что делает |
|---|---|---|
| **tool** | `add` | Складывает два числа и возвращает сумму |
| **tool** | `get_weather` | Текущая погода в городе: температура, влажность, скорость ветра |
| **resource** | `favorite-cities`<br>(`config://favorite-cities`) | JSON-список избранных городов; задаётся переменной окружения |

### **Схема инструмента** `get_weather`

| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
| `city` | `string`, минимум 1 символ | **да** | Название города: `Vilnius`, `Москва`, `Berlin` |
| `units` | `"celsius"` \| `"fahrenheit"` | нет | Единицы измерения температуры. Если не указано — берётся значение `DEFAULT_UNITS` |

Схема описана через [Zod](https://zod.dev/); SDK превращает её в JSON Schema, которую видит модель. Значения вне перечисленных (например, `kelvin`) отсекаются до попадания в код инструмента.

![Вызов инструмента add](screenshots/02-add-tool.png)

![Вызов инструмента get_weather](screenshots/03-get-weather.png)

---

## Требования

- **Node.js 18+** (рекомендуется актуальная LTS) — используются встроенные `fetch` и `AbortSignal.timeout`
- **Claude Desktop** или другой MCP-хост
- Доступ в интернет для запросов к Open-Meteo

## Установка и сборка

```bash
git clone <ссылка-на-репозиторий>
cd trainee-mcp-server
npm install
npm run build
```

После сборки появится `dist/index.js` — именно этот файл запускает хост.

Проверить, что сервер стартует:

```bash
npm start
```

Ожидаемое поведение: в консоль (stderr) выводится `MCP-сервер запущен на stdio`, после чего процесс **остаётся висеть** — он ждёт JSON-RPC-сообщения на stdin. Это нормально, выход по `Ctrl+C`.

---

## Подключение к Claude Desktop

**1.** Открой файл конфигурации:

| ОС | Путь |
|---|---|
| Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |

Быстрый путь: меню **Claude → Settings → вкладка Developer → Edit Config**.

**2.** Добавь блок сервера. Путь до `dist/index.js` должен быть **абсолютным**:

```json
{
  "mcpServers": {
    "trainee-mcp-server": {
      "command": "node",
      "args": ["C:/Users/Имя/trainee-mcp-server/dist/index.js"],
      "env": {
        "FAVORITE_CITIES": "Minsk,Gomel,Moscow",
        "DEFAULT_UNITS": "celsius",
        "REQUEST_TIMEOUT_MS": "8000"
      }
    }
  }
}
```

Блок `env` опционален — без него применяются значения по умолчанию (см. ниже).

> **Windows:** в JSON обратный слэш — управляющий символ, поэтому путь пишется либо через прямые слэши (`C:/Users/...`), либо через двойные обратные (`C:\\Users\\...`).

**3.** Полностью закрой и заново запусти Claude Desktop. Закрыть окно недостаточно — конфиг читается только при старте приложения.

**4.** Проверь подключение: **Settings → Developer** — сервер должен быть в списке. Список его инструментов виден в меню вложений рядом с полем ввода.

![Подключённые MCP-серверы](screenshots/01-filesystem-connected.png)

---

## Переменные окружения

Все переменные необязательны. Значения читаются при старте и валидируются через Zod: при некорректном значении сервер завершается с ненулевым кодом и пишет причину в stderr — вместо того чтобы молча работать с мусором.

| Переменная | Назначение | По умолчанию |
|---|---|---|
| `FAVORITE_CITIES` | Список избранных городов через запятую. Отдаётся ресурсом `favorite-cities` | `Minsk,Gomel,Moscow` |
| `DEFAULT_UNITS` | Единицы температуры, если инструмент вызван без `units`. Допустимо: `celsius`, `fahrenheit` | `celsius` |
| `REQUEST_TIMEOUT_MS` | Таймаут HTTP-запроса к Open-Meteo, мс | `8000` |

Значения в `claude_desktop_config.json` задаются **строками**, включая числовые (требование JSON) — схема приводит их к нужному типу самостоятельно.

> Сервер, запущенный из Claude Desktop, не наследует пользовательское окружение: переменные, выставленные в терминале, до него не дойдут. Задавать их нужно в блоке `env` конфига.

---

## Примеры запросов к Claude

| Что спросить | Что должно произойти |
|---|---|
| «Сколько будет 9176 плюс 912730?» | Вызов `add`, точная сумма из результата инструмента |
| «Какая сейчас погода в Вильнюсе?» | Вызов `get_weather`, температура в цельсиях |
| «Погода в Нью-Йорке в фаренгейтах» | Вызов `get_weather` с параметром `units: "fahrenheit"` |
| Приложить ресурс «Избранные города» и спросить: «Какие города в списке? Покажи погоду для первого» | Чтение ресурса, затем вызов `get_weather` для города из списка |

![get_weather с фаренгейтами](screenshots/04-get-weather-fahrenheit.png)

![Чтение ресурса favorite-cities](screenshots/09-resource-favorite-cities.png)

> Ресурс, в отличие от инструмента, модель не запрашивает сама: его прикладывает пользователь через меню вложений.

---

## Обработка ошибок

Все ошибки возвращаются как **результат вызова** с флагом `isError: true`, а не выбрасываются исключением. Разница существенная: при исключении модель получает протокольную ошибку без деталей, а так текст ошибки приходит ей как обычный ответ инструмента — и она может на него осмысленно отреагировать. Процесс сервера при этом не падает и продолжает обслуживать следующие вызовы.

| Сценарий | Что получает Claude | Как воспроизвести |
|---|---|---|
| Город не найден | Сообщение с предложением проверить написание | Спросить погоду в `asdasdasd` |
| Сервис вернул неполные данные | Сообщение о том, что город определён верно, но данных нет и повтор с другим написанием не поможет | Закомментировать установку параметра `current` в URL прогноза |
| Превышен таймаут | Сообщение с указанием лимита в секундах | Выставить `REQUEST_TIMEOUT_MS=1` |

Отдельно: `fetch` не выбрасывает исключение на статусах 4xx/5xx, поэтому `res.ok` проверяется явно. Таймаут реализован через `AbortSignal.timeout()` — без него зависший внешний сервис подвесил бы вызов инструмента на неопределённое время.

![Ошибка: город не найден](screenshots/05-error-city-not-found.png)

![Ошибка: сервис не вернул данные](screenshots/06-error-empty-response.png)

![Ошибка: таймаут](screenshots/07-error-timeout.png)

Контрольная проверка — обычный запрос после серии сбоев: сервер жив, инструмент отрабатывает штатно.

![Корректная работа после ошибок](screenshots/08-recovery-after-errors.png)

---

## Что такое MCP
**MCP** простыми словами - это "USB-порт" для подключения различных инструментов к модели. В силу того, что модель сама **не ходит в интернет**, а также писать кучу отдельных интеграций под каждый инструмент **нецелесообразно**, MCP выступает **удобным единым протоколом**.

Сам по себе **MCP-сервер** включает в себя 3 основных составляющие:
- **tools** — действия/функции, которые использует сама модель (два примера реализованы в данном тестовом задании);
- **resources** — данные для чтения, дополнительные справочники для модели (файлы, БД);
- **prompts** — уже готовые шаблоны для запросов.

---

## Структура проекта

```
trainee-mcp-server/
├── src/
│   └── index.ts        # типы, конфигурация, инструменты, ресурс, запуск
├── screenshots/        # скриншоты вызовов из Claude Desktop
├── *dist/              # результат сборки, в репозиторий не коммитится
├── package-lock.json
├── package.json
├── README.md
└── tsconfig.json
```

Логи сервера пишутся только в **stderr**: stdout занят транспортом JSON-RPC, и любой вывод туда ломает обмен сообщениями с хостом.