Agent Frameworks

Keep Mastra agents fast in production with Sanity Context

Mastra

TypeScript-first agent framework with built-in evals, memory, and workflow primitives for shipping agents that stay fast under real production load.

Visit Mastra

Your Mastra agent feels instant in the playground and then crawls in production. The same `agent.generate()` call that returned in 800ms locally now takes six seconds, and when you trace it the model isn't the bottleneck. The agent is making four sequential tool calls, pulling a 40KB JSON blob into context, then re-deciding what to fetch next. Latency is a retrieval problem wearing a model costume.

The fix is to give the agent fewer, sharper tools that return exactly the fields it needs. That's where Sanity Context comes in. Sanity Context is Sanity's agent-facing product. Its primary surface today is Context MCP, a hosted, read-only MCP endpoint that exposes schema reads, GROQ queries, reference traversal, and optional semantic search across a Sanity dataset, with Knowledge Bases as the second surface for unstructured sources like PDFs, websites, and support data.

This article stays on Mastra's side of the problem first: where production latency actually comes from in an agent loop, how to measure it, and how to cut tool-call round-trips. Then it shows how attaching Context MCP as a single schema-aware tool source replaces a pile of bespoke fetch tools with typed GROQ queries that return only what the turn needs.

Where Mastra latency actually comes from

When a Mastra agent is slow in production, the instinct is to blame the model or swap to a faster provider. Usually the model is the cheapest part of the request. The expensive part is the agent loop: every tool call is a full round-trip where the model emits a tool invocation, your code runs it, the result goes back into context, and the model decides again. Four sequential tool calls means four round-trips plus four model completions, each one carrying a context window that keeps growing.

Mastra gives you the primitives to see this. Turn on telemetry and look at the span tree for a single `agent.generate()`. You'll typically find the wall-clock time split across tool execution, not token generation. A tool that hits a slow upstream API, or one that returns a giant payload the model then has to read through on the next turn, dominates the trace.

The second hidden cost is context bloat. Each tool result is appended to the message history. If your `getProduct` tool returns the entire product document, 30 fields the agent doesn't need, every subsequent turn pays to re-read it. Token count drives both latency and cost. A 40KB JSON blob in context is not free; the model processes it on every completion until you trim the history.

So the two levers are clear before any framework choice: cut the number of round-trips, and shrink what each tool returns. Everything below is downstream of those two numbers.

Enable telemetry on a Mastra agent

Telemetry exports per-tool spans so you can see which call dominates the trace, not just the total.

import { Mastra } from '@mastra/core';
import { agent } from './agents';

export const mastra = new Mastra({
  agents: { agent },
  telemetry: {
    serviceName: 'support-agent',
    enabled: true,
    sampling: { type: 'always_on' },
    export: {
      type: 'otlp',
      endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
    },
  },
});

// Each agent.generate() now emits spans per tool call.
// Inspect the span tree to find which tool dominates wall-clock.

Cut round-trips before you cut models

Once telemetry shows tool round-trips as the cost center, the highest-leverage fix is to collapse several narrow tools into one tool that answers the agent's actual question in a single call. If your agent has `getProduct`, `getReviews`, `getRelatedProducts`, and `getPricing`, and it almost always calls all four in sequence for a product question, it is paying for three extra round-trips to assemble one answer.

Mastra's tool definition makes the shape of each call explicit through its input schema. Use that to design tools around questions, not around tables. A single `getProductContext` tool that takes a product ID and a flag for what facets to include lets the model fetch the product, its reviews, and its related items in one round-trip. The model emits one tool call instead of four, and the span tree flattens.

The other half is returning less. Define the output so the tool projects only the fields the agent reasons over. If the agent answers questions about price and availability, do not hand it the full marketing body, the SEO metadata, and the audit history. Trim at the source. This is where a typed query language matters: you want to express the projection once, at the tool boundary, and have it run server-side so the payload is small before it ever reaches context.

These two moves, fewer tools and tighter projections, routinely take a six-second turn back under two seconds without touching the model. The model was never the problem.

âš ī¸

Parallel tool calls don't fix sequential reasoning

Mastra can run independent tool calls in parallel, but if the agent needs review data to decide which related products to fetch, those calls are inherently sequential. Parallelism only helps when the calls don't depend on each other. The durable fix is fewer calls that each return more of what the next reasoning step needs, not faster-but-still-sequential ones.

Design tools around questions with the input schema

Mastra's `createTool` ties an input schema, an output schema, and an execute function together. The input schema is your contract with the model: it decides what the model is allowed to ask for, and a tight schema makes the model's tool calls more predictable and faster to validate. A loose schema invites the model to pass freeform strings it then has to reason about, which adds turns.

The pattern that keeps agents fast is one consolidated context tool whose input flags let the model request exactly the facets it needs. The execute function does the assembly server-side and returns a single trimmed object. The model gets everything for the turn in one round-trip, and the output schema guarantees the shape so downstream steps don't re-fetch to fill gaps.

The thing to resist is letting the execute function turn into a hand-rolled join across three upstream APIs. That is exactly the work that drifts: a new field gets added to one source, the tool keeps returning the old shape, and the agent starts hallucinating around the gap. You want the assembly and the projection to live in one query against one source of truth, not stitched together in TypeScript that nobody updates.

One consolidated tool instead of four narrow ones

The input flags let the model request facets in one round-trip; the output schema keeps the payload trimmed.

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

export const getProductContext = createTool({
  id: 'get-product-context',
  description: 'Fetch a product with optional reviews and related items in one call.',
  inputSchema: z.object({
    productId: z.string(),
    include: z.array(z.enum(['reviews', 'related', 'pricing'])).default([]),
  }),
  outputSchema: z.object({
    title: z.string(),
    price: z.number(),
    inStock: z.boolean(),
    reviews: z.array(z.object({ rating: z.number(), body: z.string() })).optional(),
    related: z.array(z.object({ id: z.string(), title: z.string() })).optional(),
  }),
  execute: async ({ context }) => {
    // One server-side query, projected to exactly these fields.
    return fetchProductContext(context.productId, context.include);
  },
});

Attach Context MCP so the agent gets schema-aware tools for free

Hand-writing and maintaining that consolidated tool against your content source is real work, and it drifts. This is where Sanity Context earns its place in the loop. Its primary surface, Context MCP, is a hosted, read-only MCP endpoint that exposes your dataset's schema, GROQ queries, and reference traversal as tools the agent can call directly. Mastra speaks MCP natively, so attaching it is the fastest way in: you point Mastra's MCP client at the endpoint and the agent gets schema-aware tools without you writing a single fetch wrapper.

Because the endpoint exposes the schema, the agent knows the actual field names and reference shapes ahead of time. It doesn't guess at structure and then fetch to check. Reference traversal happens server-side in one GROQ query, so the four-tools-in-sequence pattern collapses into one MCP tool call that returns the product, its references, and only the projected fields. That is the round-trip reduction from the earlier sections, handed to you instead of hand-built.

The read-only constraint is the point, not a limitation. An agent answering questions should not be able to mutate your content mid-conversation. Reads go through Context MCP; writes, when you need them, go through Agent Actions, a separate path. Keeping retrieval read-only means you can attach the endpoint to a production agent without worrying that a bad completion edits a live document.

Wire Context MCP into a Mastra agent

Mastra's MCPClient attaches the hosted Context MCP endpoint; getTools() returns the schema-aware tool set.

import { Agent } from '@mastra/core/agent';
import { MCPClient } from '@mastra/mcp';
import { openai } from '@ai-sdk/openai';

const mcp = new MCPClient({
  servers: {
    sanityContext: {
      url: new URL(process.env.SANITY_CONTEXT_MCP_URL!),
    },
  },
});

export const agent = new Agent({
  name: 'support-agent',
  instructions: 'Answer product questions using the Sanity Context tools.',
  model: openai('gpt-4o-mini'),
  tools: await mcp.getTools(),
});

// The agent now has schema-aware, read-only GROQ tools.
// No hand-written fetch wrappers to drift out of sync.

Keep retrieval structured, reach for semantic search only when it pays

There's a temptation, once retrieval is the topic, to reach straight for embeddings and a vector database. For a Mastra agent over structured content, that is usually the slow path, not the fast one. What production data on Context MCP actually shows is that the heavy majority of calls are structured: GROQ queries and schema lookups, with a compressed initial context behind them. Semantic search is a small slice, and embeddings are opt-in, off by default. Most projects shipping on Context MCP never turn them on.

The reason is latency and correctness. When the agent's question has a structural component, a date range, an author, a product variant, a publication state, a structured predicate resolves it exactly and fast. An embedding similarity search returns the nearest documents, which is the wrong answer when the user asked for the August release notes and got the visually-similar July ones. Structured retrieval is both quicker and more correct for those queries.

When you do need fuzzy matching over prose, GROQ does hybrid retrieval inside a single query rather than bolting on a separate vector store. The discipline is structured predicates plus keyword matching plus optional embeddings, ordered by score, not embeddings everywhere. You reach for the semantic layer when the agent's failures justify it, and you keep the structural predicates doing the heavy lifting.

â„šī¸

Structured retrieval is the default, not the fallback

Vector search and RAG are not the same as good retrieval; they are one ingredient. For an agent over schema'd content, GROQ queries and schema lookups handle the heavy majority of calls. Turn embeddings on when a real retrieval failure justifies them, not as a starting assumption.

Hybrid retrieval inside one GROQ query

Structural predicate first, then BM25 keyword boost and optional semantic similarity combined by score().

*[_type == "article" && publishedAt > $since]
  | score(
      boost(title match text::query($queryText), 3),
      text::semanticSimilarity($queryText)
    )
  | order(_score desc)[0...5]{
    title,
    publishedAt,
    "snippet": pt::text(body)[0...200]
  }

Unstructured sources and content that should be governed

Not all of your agent's context is structured. Support transcripts, PDF manuals, and marketing pages on a website are messy by nature, and forcing them through GROQ projections fights the data. That is what Knowledge Bases, the second Sanity Context surface, is for: it turns those sources into well-ordered documents with a clear table of contents the agent can navigate, instead of dumping raw text into context. When your Mastra agent needs to answer from a PDF manual or a support corpus, route that through Knowledge Bases and keep GROQ retrieval for the catalog and the schema'd content.

There's a governance angle that matters for production agents specifically. The agent's instructions, its approved responses, and its brand voice are content too, and they should be versioned, reviewed, and previewed before they go live, not edited in a config file and YOLO'd to prod. Sanity is the AI Content Operating System, the intelligent backend for companies building AI content operations at scale, and Content Releases ships exactly this workflow: stage a change to the agent's knowledge, preview it, and publish it as a unit. Editors and developers work against one governed source instead of a JSON blob nobody owns.

This is the division of labor that keeps a Mastra agent both fast and safe: ephemeral per-user chat history lives in your memory store, Upstash or Redis territory, while the governed, reviewable content the agent reasons over lives in Sanity Context and reaches the agent through Context MCP or Knowledge Bases.