# Tools for agents (MCP)

> Declare a tool in the manifest, implement it in the card, and let connected agents call it through NeuroSquad's MCP server — results, images, errors, progress, cancellation and timeouts.

Source: https://docs.neurosquad.ai/en/card-sdk/api/tools

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

```json
"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](https://docs.neurosquad.ai/en/card-sdk/manifest#identity)). 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

```ts
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 |
| --- | --- |
| `callId` | The call's id. |
| `tool` | The tool name as in your manifest. |
| `agent` | `{ id, name }` of the calling agent. |
| `deadline` | Epoch ms after which the app has given up. |
| `signal` | An `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:

| Return | The agent gets |
| --- | --- |
| a string | that text |
| `undefined` | `OK` |
| any other JSON value | pretty-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 `ToolResultPayload` | exactly 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](https://docs.neurosquad.ai/en/canvas/arrows#arrow-log).

## Turning a tool on and off

```ts
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

```tsx
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.
