How do I define tools for an Agent?

HEAD
be60b73Fix browser bundling for @mastra/react and Web Streams typing in @mastra/client-js (#14079)
This post might have stale content, as HEAD is 68 commits ahead.

how do i define tools for an Agent? do they automatically get "context" or some type of state they can use? how is this defined?

Avatar of anthropic/claude-opus-4.6
Claude Opus 4.6
commented

Now I have all the information I need. Here's the comprehensive answer:


##Defining Tools for an Agent

###1. Creating a Tool with createTool

You define tools using createTool from @mastra/core/tools. A tool needs an id, description, and an execute function. Optionally, you define inputSchema and outputSchema for validation:

import { createTool } from '@mastra/core/tools';
import { z } from 'zod';

const weatherTool = createTool({
  id: 'get-weather',
  description: 'Get current weather for a location',
  inputSchema: z.object({
    location: z.string().describe('City name'),
  }),
  outputSchema: z.object({
    temperature: z.number(),
    conditions: z.string(),
  }),
  execute: async (inputData, context) => {
    // inputData is typed from inputSchema: { location: string }
    // context is a ToolExecutionContext (see below)
    return { temperature: 72, conditions: 'Sunny' };
  },
});

Source: packages/core/src/tools/tool.ts lines 393–518 — the createTool function.

###2. Assigning Tools to an Agent

You pass tools as a Record<string, Tool> to the tools property of the Agent config:

import { Agent } from '@mastra/core/agent';

const agent = new Agent({
  id: 'weather-agent',
  name: 'Weather Agent',
  instructions: 'You help users with weather information',
  model: 'openai/gpt-4o',
  tools: { weatherTool },  // key = tool name the agent will use
});

Tools can also be dynamic — the tools property accepts a DynamicArgument, meaning it can be a function that resolves tools based on request context:

tools: ({ requestContext }) => {
  const tier = requestContext.get('tier');
  return tier === 'premium' ? { weatherTool, advancedTool } : { weatherTool };
}

Source: packages/core/src/agent/types.ts lines 132–240 — the AgentConfig interface.


##Yes, Tools Automatically Get Context!

The execute function's second parameter is a ToolExecutionContext that Mastra automatically injects at runtime. You don't need to set it up — it's populated by the framework depending on where the tool is called from (agent, workflow, MCP server, or direct invocation).

###The ToolExecutionContext Interface

Defined in packages/core/src/tools/types.ts lines 287–319:

interface ToolExecutionContext<TSuspend, TResume, TRequestContext> {
  // ===== Common properties (always available) =====
  mastra?: MastraUnion;              // Access to the Mastra instance
  requestContext?: RequestContext;    // Key-value store for per-request data
  abortSignal?: AbortSignal;         // For cancellation
  workspace?: Workspace;             // File system & sandbox access
  writer?: ToolStream;               // For streaming data back during execution

  // ===== Context-specific nested properties =====
  agent?: AgentToolExecutionContext;  // Present when called from an Agent
  workflow?: WorkflowToolExecutionContext;  // Present when called from a Workflow
  mcp?: MCPToolExecutionContext;      // Present when called from an MCP server
}

###Agent-specific context (context.agent)

When a tool is executed by an agent, context.agent is populated with (types.ts lines 32–48):

interface AgentToolExecutionContext<TSuspend, TResume> {
  toolCallId: string;       // Unique ID for this tool call
  messages: any[];          // Conversation messages so far
  suspend: (payload, options?) => Promise<void>;  // Suspend tool for human approval
  threadId?: string;        // Memory thread ID
  resourceId?: string;      // Memory resource ID
  resumeData?: TResume;     // Data from resuming a suspended tool
  writableStream?: WritableStream;
}

###Workflow-specific context (context.workflow)

When called from a workflow step (types.ts lines 50–60):

interface WorkflowToolExecutionContext<TSuspend, TResume> {
  runId: string;
  workflowId: string;
  state: any;
  setState: (state: any) => void;
  suspend: (payload, options?) => Promise<void>;
  resumeData?: TResume;
}

###MCP-specific context (context.mcp)

When called from an MCP server (types.ts lines 62–70):

interface MCPToolExecutionContext {
  extra: RequestHandlerExtra;  // MCP protocol context
  elicitation: {
    sendRequest: (request) => Promise<ElicitResult>;  // Interactive user input
  };
}

##How Context Gets Injected (Under the Hood)

When an agent needs to run a tool, it calls convertTools() (agent.ts line 3700](packages/core/src/agent/agent.ts)), which calls listAssignedTools() ([agent.ts line 2443). Each tool is converted via makeCoreTool(), which creates a CoreToolBuilder (builder.ts line 46`).

The CoreToolBuilder.createExecute() method ([builder.ts lines ~305–515](packages/core/src/tools/tool-builder/builder.ts)) is the adapter between the AI SDK's call signature and Mastra's (inputData, context)signature. It constructs thetoolContext` object with:

  • mastra — a proxy wrapping the Mastra instance (with tracing context)
  • requestContext — the RequestContext from the current request
  • workspace — for file/sandbox operations
  • writer — a ToolStream wrapping the output writer
  • agent/workflow/mcp — nested based on execution source detection
// From builder.ts — simplified
const baseContext = {
  mastra: wrappedMastra,
  requestContext: execOptions.requestContext ?? options.requestContext ?? new RequestContext(),
  workspace: execOptions.workspace ?? options.workspace,
  writer: new ToolStream({ prefix: 'tool', callId, name, runId }, outputWriter),
  abortSignal: execOptions.abortSignal,
  suspend: ...,
  resumeData: ...,
};

if (isAgentExecution) {
  toolContext = { ...restBaseContext, agent: { toolCallId, messages, suspend, resumeData, threadId, resourceId } };
} else if (isWorkflowExecution) {
  toolContext = { ...restBaseContext, workflow: { runId, workflowId, state, setState, suspend, resumeData } };
} else if (execOptions.mcp) {
  toolContext = { ...baseContext, mcp: execOptions.mcp };
}

##Using Context in Practice

###Accessing Mastra services (storage, logger, etc.)

const saveTool = createTool({
  id: 'save-data',
  description: 'Save data to storage',
  inputSchema: z.object({ key: z.string(), value: z.any() }),
  execute: async (inputData, context) => {
    const storage = context?.mastra?.getStorage();
    const logger = context?.mastra?.getLogger();
    logger?.debug('Saving data...');
    await storage?.set(inputData.key, inputData.value);
    return { saved: true };
  },
});

Source: doc comment in tool.ts lines 57–67.

###Checking if running in agent vs. workflow

execute: async (inputData, context) => {
  if (context?.workflow) {
    console.log(`Running in workflow ${context.workflow.workflowId}`);
  }
  if (context?.agent) {
    console.log(`Called by agent, toolCallId: ${context.agent.toolCallId}`);
  }
  return { processed: inputData.data.toUpperCase() };
}

Source: unified-integration.test.ts lines 476–496.

###Using requestContext for per-request values

// In the tool — read values from requestContext
execute: async (inputData, context) => {
  const indexName = context?.requestContext?.get('indexName') ?? 'default-index';
  const apiKey = context?.requestContext?.get('apiKey');
  // ...
}

Source: packages/rag/src/tools/vector-query.ts — a real-world example.

###Validating requestContext with requestContextSchema

You can define a requestContextSchema on a tool to validate that required request context values are present before execution:

const tool = createTool({
  id: 'secure-tool',
  description: 'Requires auth context',
  requestContextSchema: z.object({
    userId: z.string(),
    apiKey: z.string(),
  }),
  execute: async (inputData, context) => {
    const { userId, apiKey } = context.requestContext!.all;
    // Guaranteed to be valid at this point
    return { success: true };
  },
});

If the requestContext doesn't satisfy the schema, the tool returns a validation error without executing.

Source: packages/core/src/tools/__tests__/request-context-schema.test.ts.

###Streaming data from tools with writer

execute: async (inputData, context) => {
  await context?.writer?.custom({
    type: 'data-progress',
    data: { status: 'processing', taskName: inputData.taskName },
  });
  // ... do work ...
  return { result: 'done' };
}

Source: packages/core/src/tools/tool-stream.test.ts lines 158–180.

###Suspend/resume (human-in-the-loop)

Tools can also be marked requireApproval: true and use context.agent.suspend() to pause for human input:

const deleteFileTool = createTool({
  id: 'delete-file',
  description: 'Delete a file',
  requireApproval: true,
  inputSchema: z.object({ filepath: z.string() }),
  suspendSchema: z.object({ reason: z.string() }),
  resumeSchema: z.object({ approved: z.boolean() }),
  execute: async (inputData, context) => {
    if (context?.agent?.resumeData?.approved) {
      // User approved, proceed
      return { deleted: true };
    }
    await context?.agent?.suspend({ reason: 'Confirm file deletion' });
  },
});

In summary: You create tools with createTool, assign them to agents via the tools config object, and every tool's execute function automatically receives a rich ToolExecutionContext as its second argument — giving it access to mastra services, requestContext, workspace, writer, and context-specific properties for agent/workflow/MCP execution. You never need to wire this up manually.


END OF POST

How do I define tools for an Agent? — mastra-ai/mastra