MCP Tools
Tools are the primary way agents interact with your plugin. Every tool is an MCP tool — it appears in the agent’s tool list and can be called from any session.
Tool file structure
Section titled “Tool file structure”Each tool is a TypeScript file that exports two things:
const schema = { /* JSON Schema definition */ };async function handler(args, ctx) { /* implementation */ }export { schema, handler };Recursive discovers all .ts and .js files in the directory pointed to by "tools" in your manifest.
Schema definition
Section titled “Schema definition”The schema follows the MCP tool schema format:
const schema = { name: 'my_plugin_do_thing', description: 'One-line description of what this tool does.', inputSchema: { type: 'object', properties: { item_id: { type: 'string', description: 'The item to operate on. Required.', }, options: { type: 'object', properties: { verbose: { type: 'boolean', description: 'Include extra detail. Default: false.' }, }, }, }, required: ['item_id'], additionalProperties: false, }, audience: 'shared',};Schema fields
Section titled “Schema fields”| Field | Type | Description |
|---|---|---|
name | string | Tool name. Must be unique across all plugins. Convention: {pluginId}_{action}. |
description | string | Shown to agents. Be specific — agents use this to decide when to call the tool. |
inputSchema | object | JSON Schema for the tool’s parameters. |
audience | string | Who can call this tool: "orchestrator", "agent", "shared", "dashboard", or "any". |
Audience values
Section titled “Audience values”| Value | Description |
|---|---|
orchestrator | Only the orchestrator/coordinator agent. |
agent | Only child/delegated agents. |
shared | Both orchestrator and agents. Most common. |
dashboard | Only the dashboard (HTTP API). |
any | Everyone, including dashboard. |
Handler function
Section titled “Handler function”The handler receives two arguments:
async function handler(args: Record<string, unknown>, ctx: ToolContext) { // args = validated input matching your schema // ctx = platform utilities}The ctx object
Section titled “The ctx object”| Property | Type | Description |
|---|---|---|
ctx.db | StateDb | Full access to the Recursive state database. |
ctx.V | Validator | Schema-based input validation utilities. |
ctx.broadcast | function | Send SSE events to connected dashboard clients. |
ctx.getSettings | function | Read plugin settings. |
ctx.pluginId | string | The current plugin’s ID. |
Return values
Section titled “Return values”Return a plain object. Recursive automatically wraps it in the MCP tool result format.
async function handler(args, ctx) { const widget = ctx.db.getWidget(args.id); return { id: widget.id, name: widget.name, summary: `Found widget "${widget.name}".`, };}async function handler(args, ctx) { const widget = ctx.db.getWidget(args.id); if (!widget) { return { error: true, code: 'NOT_FOUND', message: `Widget "${args.id}" not found.`, fix: 'Use widget_list to find available widget IDs.', retryable: true, }; } return widget;}Error codes
Section titled “Error codes”| Code | When to use |
|---|---|
MISSING_REQUIRED | A required parameter was not provided. |
NOT_FOUND | The referenced entity doesn’t exist. |
INVALID_INPUT | Input failed validation. |
CONFLICT | Operation conflicts with current state (e.g., duplicate ID). |
INTERNAL | Unexpected server error. |
BOUNDARY_VIOLATION | Path traversal or scope violation. |
Input validation
Section titled “Input validation”Use ctx.V for structured validation:
async function handler(args, ctx) { const { valid, errors } = ctx.V.validate(args, { title: { required: true, type: 'string', minLength: 1 }, priority: { type: 'integer', min: 1, max: 4 }, });
if (!valid) { return { error: true, code: 'INVALID_INPUT', message: errors.map(e => e.message).join('; '), retryable: true, }; }
// proceed with validated args}Broadcasting events
Section titled “Broadcasting events”When your tool modifies state, broadcast an SSE event so the dashboard updates in real time:
async function handler(args, ctx) { const widget = createWidget(args);
ctx.broadcast('widgets', { action: 'created', id: widget.id, });
return widget;}The dashboard store listens for the event channel you declared in ui.sseEvents and reacts accordingly.
Best practices
Section titled “Best practices”- Name tools descriptively — agents read the name and description to decide what to call.
widget_createis better thancreate. - Always validate input — never trust that args match your schema. Use
ctx.V. - Return structured data — agents need to parse results. Return objects, not prose.
- Include a
summaryfield — human-readable summary alongside machine data. - Use
additionalProperties: false— prevents agents from hallucinating extra fields. - Document required fields in descriptions — add “Required.” to required field descriptions. Agents read these.