>_MCP · агенты · загрузка файлов
Как загрузить файл через MCP и не слить все токены
Почему агенту нельзя передавать файлы аргументом в MCP и как грузить их мимо контекста модели.
Файл можно загрузить через MCP, но передавать его прямо в вызове MCP плохая идея. Я делаю по-другому: MCP-сервер даёт агенту ссылку на загрузку, а файл агент отправляет сам через curl из терминала. В контекст модели файл не попадает, и токены на него не уходят.
У меня есть проект, где агент работает через MCP-сервер, и туда нужно загружать файлы и картинки. Когда мне понадобилось добавить картинку, агент ответил: «Картинку агенту не передать: файл кладёт человек, агент только ставит ссылку». И предложил схему, где я должен открыть браузер, зайти в хранилище и загрузить файл руками.
Меня это не устроило. Ниже рассказываю, как сделал так, чтобы агент загружал файлы сам.
Почему нельзя передать файл прямо в вызове MCP
Всё, что агент передаёт в MCP, уходит в JSON. Бинарные данные JSON не умеет, поэтому картинку приходится кодировать в строку, обычно в base64. А параметры вызова пишет сама модель, то есть и эту строку пишет она.
Что получается:
- Файл целиком проходит через контекст. Агент читает файл и переписывает его в вызов символ за символом. Картинка на 300 КБ в base64 весит около 400 КБ, это больше ста тысяч токенов.
- Агенту не нужно знать, что внутри файла. Ему надо только положить файл куда нужно и вставить ссылку в текст.
- Модель может ошибиться в base64. Она пишет строку заново. Одна ошибка в символе, и на сервер приходит битый файл.
- Лимиты. Большой PDF или презентация в один вызов просто не поместятся.
Первый раз я на это наткнулся в другой задаче. Агент сохранял на сервер готовые отчёты через MCP и передавал файл целиком в вызове. Всё работало, но каждый отчёт проходил через контекст, и агент жёг токены на то, что делается одной командой в терминале.
Решение: файл идёт мимо MCP через curl
Я разнёс это на два потока. Через MCP агент узнаёт, куда загружать, и получает результат. А сам файл отправляет обычным HTTP-запросом через curl. Модель пишет короткую команду с путём к файлу, дальше файл читает и отправляет уже curl.
По сути это то же самое, что presigned URL в S3: сервер выдаёт временную ссылку, и клиент загружает файл по ней напрямую. Только клиент тут AI-агент с терминалом.
- 01Агентпросит у MCP-сервера ссылку на загрузку
- 02MCP-сервервыдаёт ссылку на 5 минут и готовую команду curl
- 03curlотправляет файл мимо контекста модели
- 04Серверпроверяет файлы и возвращает готовый markdown
У меня два варианта:
- Ссылка от MCP. Агент вызывает инструмент MCP, получает ссылку и готовую команду
curlи запускает её. Подходит, когда заранее неизвестно, какие будут файлы и куда их класть. - MCP только подтверждает. Агент по инструкции из скилла загружает файл через
curlв хранилище и сообщает MCP, что файл загружен. MCP-сервер и хранилище живут на одном сервере и строят путь к файлу по одному правилу, так что ссылку передавать не нужно. Сервер проверяет, что файл на месте и не пустой, и отмечает задачу выполненной. Подходит, когда заранее известно, какой файл и куда.
В обоих вариантах агент файл не читает. Модель видит только короткий запрос и короткий ответ.
Как агент загружает картинку: пошагово
Покажу на первом варианте. Имена и адреса условные.
Шаг 1. Агент получает ссылку
Агент вызывает инструмент MCP и передаёт только ID записи, к которой относятся файлы. Сервер возвращает ссылку, которая живёт 5 минут и работает только для этой записи, и готовую команду:
{
"ok": true,
"upload_url": "https://example.com/upload/<ключ>",
"expires_at": "09.10.2026 15:05",
"curl": "curl -sS -F \"file1=@/путь/к/картинке.png\" \"https://example.com/upload/<ключ>\""
}
В описании инструмента прямо написано: файлы через MCP не передавать, curl запускать там, где лежат файлы. Без этого модель иногда всё равно пытается засунуть файл в вызов.
Шаг 2. Агент запускает curl
curl -sS -F "file1=@./Схема.png" -F "file2=@./Отчёт.pdf" "https://example.com/upload/<ключ>"
За один запрос можно отправить несколько файлов, у каждого своё поле. В команде только пути к файлам, так что токенов она почти не тратит.
Шаг 3. Сервер сохраняет файлы
Сервер проверяет ссылку, кладёт файлы в папку записи и сразу переименовывает: «Схема.png» превращается в skhema.png. В ответе готовые строки markdown, которые агент вставляет в текст:
{
"ok": true,
"saved": [
{"file": "skhema.png", "markdown": ""},
{"file": "otchet.pdf", "markdown": "[отчёт](files/otchet.pdf)"}
],
"errors": [],
"next": "Вставь строки из saved[].markdown в текст и сохрани запись."
}
Поле next подсказывает агенту следующий шаг. Ответ curl агент читает так же, как ответ MCP.
Шаг 4. Агент сохраняет запись
Агент вставляет строки в текст, правит подписи и сохраняет запись через MCP.
В итоге я пишу агенту одну фразу: «загрузи картинки из этой папки и вставь их в текст». Дальше он всё делает сам.
Эндпоинт загрузки на сервере
Упрощённый пример на TypeScript для Node.js с Express и multer. Как генерировать и проверять ключ, не показываю, это вы пишете сами:
import express, { type NextFunction, type Request, type Response } from "express";
import multer from "multer";
import { MAX_FILE_BYTES, MAX_FILES } from "./config";
import { storeFile, type SavedFile } from "./storage";
type Grant = { recordId: string };
export function makeUploadKey(recordId: string): string {
...
}
export function verifyUploadKey(key: string): Grant | null {
...
}
const app = express();
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: MAX_FILE_BYTES, files: MAX_FILES },
});
app.get("/upload/:key", (_req: Request, res: Response) => {
res.status(405).json({
ok: false,
message: 'Только POST multipart/form-data: curl -F "file1=@файл.png" <эта ссылка>',
});
});
app.post(
"/upload/:key",
(req: Request, res: Response, next: NextFunction) => {
const grant = verifyUploadKey(req.params.key);
if (grant === null) {
res.status(401).json({ ok: false, message: "Ссылка неверная или истекла. Получи новую через MCP." });
return;
}
res.locals.grant = grant;
next();
},
upload.any(),
async (req: Request, res: Response) => {
const grant = res.locals.grant as Grant;
const files = (req.files ?? []) as Express.Multer.File[];
const saved: SavedFile[] = [];
const errors: { name: string; error: string }[] = [];
for (const file of files) {
const result = await storeFile(grant, file);
if ("error" in result) {
errors.push({ name: file.originalname, error: result.error });
continue;
}
saved.push(result);
}
res.status(saved.length === 0 ? 422 : 200).json({ ok: errors.length === 0, saved, errors });
},
);
app.listen(3000);
Тексты ошибок я пишу для модели. На 405 агент видит правильную команду, на 401 понимает, что нужна новая ссылка. Подробнее об этом я писал в статье «MCP: как дать AI доступ к вашим продуктам и данным».
Безопасность: что проверять при загрузке
Агенту я доверяю не больше, чем любой форме на сайте. Поэтому проверяю:
- Срок жизни ссылки. У меня 5 минут. Агенту хватает, а старая ссылка из логов или переписки уже не сработает.
- Привязка к записи. По ссылке для одной записи нельзя загрузить файл в другую. Подделать или продлить ссылку на стороне клиента нельзя.
- Расширения и размер. Принимаю только то, что нужно. Обычно это картинки и офисные документы.
- Содержимое файла. Если прислать текстовый файл с расширением
.png, сервер вернёт ошибку «Это не картинка». - Никаких SVG. В SVG можно встроить скрипт. Если файлы отдаются с домена, где пользователи залогинены, скрипт получит доступ к их сессии.
- Имя файла задаёт сервер. Из присланного имени делается только ключ: транслит, нижний регистр, дефисы. Путь вида
../../.envне пройдёт. - Логи. Каждая загрузка пишется в лог: какие файлы и куда.
Где это работает, а где нет
Нужен агент с терминалом и доступом к файлам: Claude Code, Cowork, Cursor или свой агент на сервере. В обычном чате claude.ai или ChatGPT агент не видит файлы на вашем компьютере, так что отправить их ему нечем.
Поэтому ручную загрузку я не убирал. В инструкции MCP-сервера написано: если есть файлы и терминал, загружай сам, если нет, дай человеку ссылку для ручной загрузки. Агент сам решает, как поступить.
Ограничения
- Агенту нужен выход в интернет из терминала. Если песочница закрыта, домен для загрузки надо разрешить отдельно.
- Клиент может спросить разрешение на запуск
curl: команда отправляет данные наружу. - Если агент долго искал файлы и ссылка протухла, он получит 401 и запросит новую.
- Файл с тем же именем перезаписывает старый. Для картинок в тексте это удобно, для архива документов нет.
- Картинку агент не видит. Если подпись нужна по содержанию, ему придётся открыть файл отдельно.
Что запомнить
- Параметры вызова MCP пишет модель, поэтому всё, что туда попадает, идёт через контекст и стоит токенов.
- Файлы в вызове MCP я не передаю. MCP выдаёт ссылку или правило, а файл уходит обычным HTTP через
curl. - Ответ сервера при загрузке пишу для модели: готовые строки для вставки, подсказка, что делать дальше, понятные ошибки.
- Для клиентов без терминала оставляю ручную загрузку.
Частые вопросы
Можно ли передать файл через MCP?
Можно, если закодировать его в base64 и передать строкой в вызове. Но тогда файл целиком идёт через контекст модели, сжигает токены, и модель может его испортить. Надёжнее, когда MCP выдаёт ссылку на загрузку.
Чем это лучше base64?
Модель пишет одну короткую команду вместо сотен тысяч символов. Файл доходит без искажений, а размер ограничен только настройками сервера.
Работает ли это в обычном чате Claude или ChatGPT?
Нет. В чате у агента нет терминала и доступа к вашим файлам. Там файл придётся загрузить вручную через браузер.
Безопасно ли давать агенту ссылку на загрузку?
Если сервер проверяет всё, что перечислено выше, это не опаснее обычной формы загрузки на сайте.
Нужен ли для этого отдельный сервис?
Нет. Загрузку принимает один обработчик в том же бэкенде, где работает MCP-сервер.
Если нужно, чтобы агенты работали с файлами в вашем продукте, напишите мне. Помогу сделать MCP-сервер и загрузку файлов.