Claude как «руки» на твоём сервере
Обычный чат с Claude — это диалог: ты пишешь, он отвечает текстом. Но Claude может БОЛЬШЕ: ему можно дать «руки» — функции, которые он вызывает сам когда нужно. Скажешь «обнови цену в каталоге на 1500» — и он сам читает файл, меняет цифру, сохраняет. Это и называют «agent» или «tool use». Разберёмся как это устроено и как поставить себе.
Зачем вообще это нужно
Реальный кейс: у тебя на VPS лежит сайт, в нём JSON с товарами. Каждый раз когда поставщик меняет цену — ты заходишь по SSH, открываешь файл в nano, меняешь цифру, сохраняешь. Скучно, ошибки случаются, времени жалко. Альтернатива — Claude-агент: ты пишешь в Telegram-бота «обнови цену товара X на Y», бот шлёт это Claude, Claude вызывает функцию write_file и обновляет JSON. То же самое с базами данных, конфигами, статикой блога — любой контент, который раньше требовал ручного захода на сервер.
Простой путь: Claude Code на сервере
Если не хочешь писать своё — Anthropic уже сделал готовый CLI-агент Claude Code. Заходишь по SSH на сервер, ставишь
переходишь в папку проекта, запускаешь claude. Дальше пишешь словами что нужно — Claude сам читает/пишет файлы, запускает команды, делает коммиты. Это лучший вариант для разработки и одноразовых правок. Для постоянной автоматизации (бот, веб-интерфейс) — нужен свой агент через API.
Свой агент: tool use через API
API Anthropic принимает параметр tools — массив с описанием функций, которые Claude может вызвать. Каждая функция: имя, описание (что делает простыми словами), JSON-схема входа. Claude в ответе отдаёт либо обычный текст, либо блок tool_use — мол, «вызови функцию write_file с такими-то аргументами». Твой код выполняет эту функцию и возвращает результат через tool_result. Цикл повторяется пока Claude не скажет «готово» (stop_reason ≠ tool_use). У OpenAI это называется function calling — концепция та же.
Главное правило: песочница и whitelist
Claude — мощный, но не идеальный. Если ты дашь ему run_command(any_command) — однажды он по ошибке вызовет
и сервер кончится. Правила безопасности: 1) Никаких «универсальных» тулов вроде exec_shell — только узкие функции с конкретной задачей (update_price, add_product). 2) Whitelist путей — разрешаем читать/писать ТОЛЬКО ./data/, всё что вне — отказываем. 3) Логировать каждый tool_use в файл — чтобы потом расследовать что произошло. 4) Для production-данных — отдельный read_only-режим, где Claude может смотреть, но не менять. 5) Лимит на число итераций цикла — 10-20 максимум, иначе риск зацикливания.
Куда положить ключ ANTHROPIC_API_KEY
Берёшь на console.anthropic.com → API Keys → Create. Кладёшь в .env на сервере: ANTHROPIC_API_KEY=sk-ant-.... Никогда не давать NEXT_PUBLIC_ или VITE_ префикс — этот ключ ТОЛЬКО серверный. Если сервер у тебя на Hetzner / DigitalOcean — заведи .env в папке проекта и убедись что .env есть в .gitignore. Для systemd-сервиса — переменные либо в
либо через loadEnvFile в Node.js 20.6+.
// agent.mjs — запусти как: node agent.mjs
// Установка: npm i @anthropic-ai/sdk
import Anthropic from '@anthropic-ai/sdk';
import { readFile, writeFile } from 'node:fs/promises';
import readline from 'node:readline/promises';
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
// === ОПИСАНИЕ ТУЛОВ для Claude ===
// Claude увидит эти описания и решит когда вызывать.
const tools = [
{
name: 'read_file',
description: 'Прочитать файл из папки ./data/. Возвращает содержимое.',
input_schema: {
type: 'object',
properties: {
path: { type: 'string', description: 'Путь, например ./data/products.json' },
},
required: ['path'],
},
},
{
name: 'write_file',
description: 'Перезаписать файл новым содержимым. Только в ./data/.',
input_schema: {
type: 'object',
properties: {
path: { type: 'string' },
content: { type: 'string' },
},
required: ['path', 'content'],
},
},
];
// === ОБРАБОТЧИКИ — что РЕАЛЬНО делать когда Claude вызвал tool ===
const handlers = {
read_file: async ({ path }) => {
if (!path.startsWith('./data/')) return { error: 'Доступ только к ./data/' };
return { content: await readFile(path, 'utf-8') };
},
write_file: async ({ path, content }) => {
if (!path.startsWith('./data/')) return { error: 'Доступ только к ./data/' };
await writeFile(path, content);
return { ok: true };
},
};
// === ЦИКЛ АГЕНТА ===
async function runAgent(userMessage) {
const messages = [{ role: 'user', content: userMessage }];
let iterations = 0;
while (iterations++ < 10) { // лимит — защита от зацикливания
const r = await client.messages.create({
model: 'claude-sonnet-4-6',
max_tokens: 1024,
tools,
messages,
});
messages.push({ role: 'assistant', content: r.content });
if (r.stop_reason !== 'tool_use') {
// Claude закончил — отдаём текст пользователю
return r.content.find((b) => b.type === 'text')?.text ?? 'готово';
}
// Выполняем все запрошенные тулы и возвращаем результаты
const toolUses = r.content.filter((b) => b.type === 'tool_use');
const results = [];
for (const use of toolUses) {
console.log('→ tool:', use.name, use.input);
const handler = handlers[use.name];
const result = handler ? await handler(use.input) : { error: 'Нет такого тула' };
results.push({
type: 'tool_result',
tool_use_id: use.id,
content: JSON.stringify(result),
});
}
messages.push({ role: 'user', content: results });
}
}
// === ДЕМО ===
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const ask = await rl.question('Что сделать? ');
const answer = await runAgent(ask);
console.log('\n→', answer);
rl.close();
// Пример запроса:
// "В файле ./data/products.json у товара 'Молоко' измени price на 95"
// Claude САМ: 1) read_file → видит JSON 2) меняет цифру 3) write_file 4) пишет "готово"Минимальный агент: Claude меняет JSON-конфиг через две функции read_file/write_file