Инструменты для агентов (MCP)
Карточка может дать агентам новые возможности. Объявите инструмент в манифесте, реализуйте его в карточке — и каждый агент, соединённый с карточкой стрелкой, увидит его в своём списке инструментов через MCP-сервер, который приложение и так запускает для своих агентов (Claude Code, Codex, Qwen Code). Разрешение не нужно: согласие — это стрелка, которую проводит пользователь.
Объявить
"tools": [
{
"name": "run_tests",
"title": "Run the tests",
"description": "Runs the project's tests in the connected terminal and returns the failing tests with their messages, or 'All tests passed'.",
"inputSchema": {
"type": "object",
"properties": {
"filter": { "type": "string", "maxLength": 200, "description": "Only tests whose name contains this" }
}
},
"timeoutMs": 120000
}
]Агент видит его как <card name with - → _>_<tool> — test_radar_run_tests для карточки с именем
test-radar — с вашей inputSchema и необязательным аргументом card, который добавляет приложение
(идентификатор или имя — чтобы выбрать одну карточку, когда подключено несколько копий). Описание
доходит до модели с префиксом [Custom card "<displayName>" by <author>, community code]. Если два
пакета дадут одно и то же имя, его сохраняет установленный первым, а более поздний получает _2,
_3.
Имена, которые приложение не пропустит при установке: итоговое имя, совпадающее с собственным
инструментом NeuroSquad (canvas_spawn_card, terminal_send_keys, browser_navigate…), любое имя
с __ (зарезервировано за MCP-серверами, которые ставит пользователь), и name карточки, совпадающий
со встроенным семейством вроде telegram, squad, ports или mcp (см.
зарезервированные имена). Обновление, которое добавляет инструмент или
меняет формулировку его описания, снова спрашивает пользователя.
Пишите описание для модели: что инструмент делает, когда его использовать и что он возвращает.
Аргументов — поменьше и с ограничениями (enum, maxLength, minimum…); приложение проверяет их
до того, как их увидит ваша карточка.
Реализовать
import { toolError } from '@neurosquad/card-sdk'
card.tools.handle<{ filter?: string }>('run_tests', async ({ filter }, call) => {
call.progress('running the tests…') // shown on the arrow
const terminal = (await card.agents.list()).find((a) => a.kind === 'shell' && a.connected)
if (!terminal) return toolError('Connect a terminal card to the Test radar card first.')
const result = await card.terminals.run(terminal.id, `npm test -- ${filter ?? ''}`, {
timeoutMs: 110_000
})
if (call.signal.aborted) return // the agent gave up; nothing is sent
return result.exitCode === 0 ? 'All tests passed' : result.output
})Обработчик получает проверенные аргументы и объект call:
call. | Что это |
|---|---|
callId | Идентификатор вызова. |
tool | Имя инструмента, как в вашем манифесте. |
agent | { id, name } вызывающего агента. |
deadline | Время (мс с эпохи), после которого приложение перестаёт ждать. |
signal | AbortSignal, срабатывает при отмене или по дедлайну. |
progress(message) | Короткая строка (до 200 символов) на стрелке, пока вы работаете; не чаще 4 раз в секунду. |
То, что вы возвращаете, становится результатом инструмента:
| Возвращаете | Агент получает |
|---|---|
| строку | этот текст |
undefined | OK |
| любое другое значение JSON | отформатированный JSON текстом |
toolText(text) | текст (то же, что строка) |
toolImage(base64, 'image/png', caption?) | картинку и подпись текстом |
toolError(message) | неудачный результат (isError: true), который модель может прочитать и учесть |
ToolResultPayload | ровно его: { content: [{ type: 'text', text } or { type: 'image', data, mimeType }], isError? }, 1–16 частей |
Исключение тоже возвращает агенту ошибку — с текстом ошибки (и пишет предупреждение в журнал карточки). Результат — до 1 МБ.
Время
- Тайм-аут по умолчанию 30 с или
timeoutMsинструмента (до 120 с). По дедлайну приложение сообщает агенту, что время вышло, и отправляет карточкеtools.cancel—call.signalсрабатывает, а всё, что вы вернёте после этого, отбрасывается. - Вызовы, пришедшие до регистрации обработчика (например, пока карточка ещё грузит данные), ждут
handle()до своего дедлайна. - Вызов к карточке, чья страница усыплена, будит её (до 10 с); если её воркспейс не открыт, агенту говорят открыть воркспейс, где лежит эта карточка.
- Каждый вызов идёт по стрелке: её подпись показывает инструмент и ваши строки прогресса, а сам вызов остаётся в журнале стрелки.
Включить и выключить инструмент
await card.tools.setEnabled('run_tests', false) // hidden from agents (e.g. until the user signs in)
await card.tools.setEnabled('run_tests', true)Это действует на все копии вашей карточки.
С React
import { useTool } from '@neurosquad/card-sdk/react'
export function Scratchpad({ text }: { text: string }) {
useTool('read_scratchpad', () => text || '(empty)') // the latest `text` is always used
return <pre>{text}</pre>
}Результаты инструментов попадают прямо в контекст агента. Всё, что вы возвращаете из интернета,
файла или от пользователя, считайте недоверенным: указывайте, откуда это, держите коротким и
никогда не позволяйте инструменту, который может вызвать агент, молча сделать что-то
разрушительное — сначала спросите пользователя через card.ui.confirm.