Share what a Mastra agent produced

A Mastra agent finishes its run and returns text. A workflow finishes and returns whatever the last step's output schema declared. Both end up in a server response, and neither is something you can forward to a colleague.

If the agent built an HTML report, the best case is that the report is now a very large string in a JSON body.

Option one — a tool

Mastra's tools are typed, so the contract is visible to you and to the model:

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

export const publishReport = createTool({
  id: "publish-report",
  description: "Publish an HTML report and return its shareable URL.",
  inputSchema: z.object({ title: z.string(), html: z.string() }),
  outputSchema: z.object({ url: z.string() }),
  execute: async ({ context }) => {
    const res = await fetch("https://commareports.com/api/v1/reports", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.COMMA_API_TOKEN}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ title: context.title, html: context.html }),
    });
    if (!res.ok) throw new Error(`publish failed: ${res.status}`);
    const { url } = await res.json();
    return { url };
  },
});

Then say so in the agent's instructions — a tool the agent is not told to reach for is a tool it will use sometimes:

instructions: `Do the analysis. Render it as a self-contained HTML report,
publish it with publish-report, and reply with only the URL.`,

Option two — MCPClient

import { MCPClient } from "@mastra/mcp";

const mcp = new MCPClient({
  servers: {
    comma: {
      url: new URL("https://commareports.com/api/mcp"),
      requestInit: {
        headers: { Authorization: `Bearer ${process.env.COMMA_API_TOKEN}` },
      },
    },
  },
});

const agent = new Agent({ name: "analyst", tools: await mcp.getTools() });

Publishing, updating and reading comments arrive together, and the same configuration works from your editor — see MCP setup.

Workflows: publish as the last step

const publishStep = createStep({
  id: "publish",
  inputSchema: z.object({ title: z.string(), html: z.string() }),
  outputSchema: z.object({ url: z.string() }),
  execute: async ({ inputData }) => ({ url: await publish(inputData) }),
});

Put it before any suspend. A workflow that pauses for human approval and hands over a raw payload is asking someone to review a document they cannot read; a workflow that pauses after publishing hands over a page with comment threads, and the resume value can be what the reviewer wrote — see commenting on HTML.

Worth knowing

  • Deployed means no filesystem. On Mastra Cloud or any container, writing a file produces nothing a reader can reach — the same problem CI jobs have.
  • Keep the URL in workflow state, not the HTML. Large strings get carried through every subsequent step.
  • reports:write is enough to publish — see scoped tokens for AI agents.
  • 5 MB per report body.

Try it

Free — unlimited reports, commenters and revisions.

See MCP setup →

Related