Skip to Content
Card SDKAPI referenceTools for agents (MCP)

Tools for agents (MCP)

A card can give agents new abilities. Declare a tool in the manifest, implement it in the card, and every agent connected to the card by an arrow sees it in its tool list — through the MCP server the app already runs for its agents (Claude Code, Codex, Qwen Code). No permission is needed: the arrow the user draws is the consent.

Declare

"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 } ]

The agent sees it as <card name with - → _>_<tool> — test_radar_run_tests for a card named test-radar — with your inputSchema plus an optional card argument the app adds (an id or a name, to pick one card when several copies are connected). The description reaches the model prefixed with [Custom card "<displayName>" by <author>, community code]. If two packages would produce the same name, the one installed first keeps it and the later one gets _2, _3.

Names the app refuses at install: an exposed name equal to one of NeuroSquad’s own tools (canvas_spawn_card, terminal_send_keys, browser_navigate…), any name containing __ (reserved for MCP servers the user installs), and a card name that is a built-in family such as telegram, squad, ports or mcp (see reserved names). An update that adds a tool or rewords a tool’s description asks the user again.

Write the description for a model: what the tool does, when to use it, and what it returns. Keep arguments few and constrained (enum, maxLength, minimum…); the app validates them before your card sees them.

Implement

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 })

The handler gets the validated arguments and a call:

call.What
callIdThe call’s id.
toolThe tool name as in your manifest.
agent{ id, name } of the calling agent.
deadlineEpoch ms after which the app has given up.
signalAn AbortSignal, aborted on cancellation or at the deadline.
progress(message)A short line (≤ 200) shown on the arrow while you work; throttled to 4 a second.

What you return becomes the tool result:

ReturnThe agent gets
a stringthat text
undefinedOK
any other JSON valuepretty-printed JSON text
toolText(text)text (same as a string)
toolImage(base64, 'image/png', caption?)an image, and the caption as text
toolError(message)a failed result (isError: true) the model can read and react to
a ToolResultPayloadexactly that: { content: [{ type: 'text', text } or { type: 'image', data, mimeType }], isError? }, 1–16 parts

Throwing also returns an error to the agent, with the error’s message (and writes a warning to your card log). Results are ≤ 1 MB.

Timing

  • Default timeout 30 s, or the tool’s timeoutMs (up to 120 s). At the deadline, the app tells the agent the call timed out and sends your card tools.cancel — call.signal aborts, and whatever you return afterwards is dropped.
  • Calls that arrive before your handler is registered (say, while the card is still loading its data) wait for handle() until their deadline.
  • A call to a card whose page is suspended wakes it (up to 10 s); if its workspace is not open the agent is told to open the workspace that has the card.
  • Every call runs on the arrow: its label shows the tool and your progress lines, and it is kept in the arrow log.

Turning a tool on and off

await card.tools.setEnabled('run_tests', false) // hidden from agents (e.g. until the user signs in) await card.tools.setEnabled('run_tests', true)

This applies to every copy of your card.

With 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> }

Tool results go straight into an agent’s context. Treat anything you return that came from the web, a file or a user as untrusted: say where it came from, keep it short, and never let a tool that the agent can call silently do something destructive — ask the user with card.ui.confirm first.