Skip to main content
FastMCP supports streaming partial results from tools while they’re still executing, enabling responsive UIs and real-time feedback. This is particularly useful for long-running operations that generate content incrementally.

Streaming Output

To enable streaming for a tool, add the streamingHint annotation and use the streamContent method:
import { FastMCP } from "fastmcp";
import { z } from "zod";

const server = new FastMCP({
  name: "My Server",
  version: "1.0.0",
});

server.addTool({
  name: "generateText",
  description: "Generate text incrementally",
  parameters: z.object({
    prompt: z.string(),
  }),
  annotations: {
    streamingHint: true, // Signals this tool uses streaming
    readOnlyHint: true,
  },
  execute: async (args, { streamContent }) => {
    // Send initial content immediately
    await streamContent({ type: "text", text: "Starting generation...\n" });

    // Simulate incremental content generation
    const words = "The quick brown fox jumps over the lazy dog.".split(" ");
    for (const word of words) {
      await streamContent({ type: "text", text: word + " " });
      await new Promise((resolve) => setTimeout(resolve, 300)); // Simulate delay
    }

    // When using streamContent, you can:
    // 1. Return void (if all content was streamed)
    // 2. Return a final result (which will be appended to streamed content)

    // Option 1: All content was streamed
    return;

    // Option 2: Return final content that will be appended
    // return "Generation complete!";
  },
});

Benefits of Streaming

Streaming provides several advantages for long-running operations:

Responsive UIs

Users see results immediately instead of waiting for completion

Progressive Generation

Perfect for AI text generation, data processing, and file operations

Better UX

Users can start reading or acting on partial results

Error Recovery

Users see what was generated before an error occurred

Content Types

Streaming works with all content types:
server.addTool({
  name: "streamText",
  description: "Stream text content",
  annotations: { streamingHint: true },
  execute: async (args, { streamContent }) => {
    await streamContent({
      type: "text",
      text: "First chunk\n",
    });

    await streamContent({
      type: "text",
      text: "Second chunk\n",
    });

    return "Final chunk";
  },
});

Progress Reporting

Tools can report numeric progress using reportProgress in the context object:
server.addTool({
  name: "download",
  description: "Download a file",
  parameters: z.object({
    url: z.string(),
  }),
  execute: async (args, { reportProgress }) => {
    await reportProgress({
      progress: 0,
      total: 100,
    });

    // Simulate download progress
    for (let i = 0; i <= 100; i += 10) {
      await new Promise((resolve) => setTimeout(resolve, 500));
      await reportProgress({
        progress: i,
        total: 100,
      });
    }

    return "Download complete";
  },
});

Combining Streaming and Progress

You can use both streaming content and progress reporting together:
server.addTool({
  name: "processData",
  description: "Process data with streaming updates",
  parameters: z.object({
    datasetSize: z.number(),
  }),
  annotations: {
    streamingHint: true,
  },
  execute: async (args, { streamContent, reportProgress }) => {
    const total = args.datasetSize;

    for (let i = 0; i < total; i++) {
      // Report numeric progress
      await reportProgress({ progress: i, total });

      // Stream intermediate results
      if (i % 10 === 0) {
        await streamContent({
          type: "text",
          text: `Processed ${i} of ${total} items...\n`,
        });
      }

      await new Promise((resolve) => setTimeout(resolve, 50));
    }

    return "Processing complete!";
  },
});

Real-World Examples

AI Text Generation

server.addTool({
  name: "generateStory",
  description: "Generate a story with streaming output",
  parameters: z.object({
    prompt: z.string(),
  }),
  annotations: {
    streamingHint: true,
    readOnlyHint: true,
  },
  execute: async ({ prompt }, { streamContent, reportProgress }) => {
    await streamContent({
      type: "text",
      text: `# Generating story from: "${prompt}"\n\n`,
    });

    // Simulate AI streaming (in reality, you'd call an AI API)
    const paragraphs = [
      "Once upon a time, in a land far away...",
      "The hero embarked on an epic journey...",
      "Through trials and tribulations...",
      "Finally, peace was restored to the kingdom.",
    ];

    for (let i = 0; i < paragraphs.length; i++) {
      await reportProgress({
        progress: i + 1,
        total: paragraphs.length,
      });

      await streamContent({
        type: "text",
        text: paragraphs[i] + "\n\n",
      });

      await new Promise((resolve) => setTimeout(resolve, 1000));
    }

    return "--- The End ---";
  },
});

File Processing

server.addTool({
  name: "processFiles",
  description: "Process multiple files with progress",
  parameters: z.object({
    files: z.array(z.string()),
  }),
  annotations: {
    streamingHint: true,
  },
  execute: async ({ files }, { streamContent, reportProgress }) => {
    await streamContent({
      type: "text",
      text: `Processing ${files.length} files...\n\n`,
    });

    for (let i = 0; i < files.length; i++) {
      const file = files[i];

      await reportProgress({
        progress: i,
        total: files.length,
      });

      await streamContent({
        type: "text",
        text: `[${i + 1}/${files.length}] Processing ${file}...\n`,
      });

      // Process file (simulated)
      await new Promise((resolve) => setTimeout(resolve, 500));

      await streamContent({
        type: "text",
        text: `✓ ${file} processed\n`,
      });
    }

    await reportProgress({
      progress: files.length,
      total: files.length,
    });

    return "\nAll files processed successfully!";
  },
});

Data Analysis

server.addTool({
  name: "analyzeData",
  description: "Analyze dataset with incremental results",
  parameters: z.object({
    dataset: z.string(),
  }),
  annotations: {
    streamingHint: true,
    readOnlyHint: true,
  },
  execute: async ({ dataset }, { streamContent, reportProgress }) => {
    const steps = [
      { name: "Loading data", duration: 1000 },
      { name: "Cleaning data", duration: 800 },
      { name: "Analyzing patterns", duration: 1200 },
      { name: "Generating insights", duration: 1000 },
      { name: "Creating visualizations", duration: 900 },
    ];

    await streamContent({
      type: "text",
      text: `# Data Analysis: ${dataset}\n\n`,
    });

    for (let i = 0; i < steps.length; i++) {
      const step = steps[i];

      await reportProgress({
        progress: i,
        total: steps.length,
      });

      await streamContent({
        type: "text",
        text: `⏳ ${step.name}...\n`,
      });

      await new Promise((resolve) => setTimeout(resolve, step.duration));

      await streamContent({
        type: "text",
        text: `✓ ${step.name} complete\n\n`,
      });
    }

    await reportProgress({
      progress: steps.length,
      total: steps.length,
    });

    return "## Analysis Complete\n\nKey insights:\n- Finding 1\n- Finding 2\n- Finding 3";
  },
});

Tool Annotations

When using streaming, set appropriate annotations:
server.addTool({
  name: "streamingTool",
  description: "A tool that streams content",
  annotations: {
    streamingHint: true,      // Indicates streaming support
    readOnlyHint: true,       // Tool doesn't modify environment
    openWorldHint: true,      // Tool may interact with external entities
  },
  execute: async (args, { streamContent }) => {
    // Implementation
  },
});

Best Practices

1

Use streamingHint Annotation

Always set streamingHint: true when using streamContent to inform clients about streaming support:
annotations: {
  streamingHint: true,
}
2

Provide Immediate Feedback

Stream the first chunk quickly to show users the tool is working:
await streamContent({ type: "text", text: "Starting...\n" });
3

Update Progress Regularly

Report progress at meaningful intervals (e.g., every 10%):
if (i % 10 === 0) {
  await reportProgress({ progress: i, total });
}
4

Handle Errors Gracefully

If streaming fails, users still see what was generated:
try {
  await streamContent({ type: "text", text: chunk });
} catch (error) {
  // Handle error, possibly by streaming error message
  await streamContent({ type: "text", text: "Error: " + error.message });
  throw error;
}
5

Return vs Stream Final Content

You can either return final content (appended) or stream everything:
// Option 1: Stream everything, return void
await streamContent({ type: "text", text: "Done!" });
return;

// Option 2: Stream partial, return final
await streamContent({ type: "text", text: "Processing..." });
return "Complete!";

Next Steps

Logging

Learn how to add custom logging to your tools

Authentication

Secure your streaming tools with authentication

Build docs developers (and LLMs) love