Skip to main content

What are Tools?

Tools in MCP allow servers to expose executable functions that can be invoked by clients and used by LLMs to perform actions. They are the primary way to add functionality to your MCP server. FastMCP uses the Standard Schema specification for defining tool parameters, which means you can use your preferred schema validation library (Zod, ArkType, or Valibot).

Basic Usage

Add a tool to your server using the addTool method:
import { FastMCP } from "fastmcp";
import { z } from "zod";

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

server.addTool({
  name: "add",
  description: "Add two numbers",
  parameters: z.object({
    a: z.number().describe("The first number"),
    b: z.number().describe("The second number"),
  }),
  execute: async (args) => {
    return String(args.a + args.b);
  },
});

Schema Validation

FastMCP supports multiple schema validation libraries through the Standard Schema specification.
import { z } from "zod";

server.addTool({
  name: "fetch-zod",
  description: "Fetch the content of a url (using Zod)",
  parameters: z.object({
    url: z.string(),
  }),
  execute: async (args) => {
    return await fetchWebpageContent(args.url);
  },
});

Tools Without Parameters

You can create tools that don’t require any parameters:
server.addTool({
  name: "sayHello",
  description: "Say hello",
  execute: async () => {
    return "Hello, world!";
  },
});
Both approaches are fully compatible with all MCP clients, including Cursor.

Return Types

Tools can return different types of content:

Returning a String

The simplest return type - automatically wrapped as text content:
server.addTool({
  name: "greet",
  description: "Greet someone",
  parameters: z.object({
    name: z.string(),
  }),
  execute: async (args) => {
    return `Hello, ${args.name}!`;
  },
});

Returning Content Array

For multiple messages or mixed content types:
server.addTool({
  name: "download",
  description: "Download a file",
  parameters: z.object({
    url: z.string(),
  }),
  execute: async (args) => {
    return {
      content: [
        { type: "text", text: "First message" },
        { type: "text", text: "Second message" },
      ],
    };
  },
});

Returning an Image

Use the imageContent helper to return images:
import { imageContent } from "fastmcp";

server.addTool({
  name: "download",
  description: "Download a file",
  parameters: z.object({
    url: z.string(),
  }),
  execute: async (args) => {
    // From URL
    return imageContent({
      url: "https://example.com/image.png",
    });

    // From file path
    // return imageContent({
    //   path: "/path/to/image.png",
    // });

    // From buffer
    // return imageContent({
    //   buffer: Buffer.from("...", "base64"),
    // });
  },
});

Returning Audio

Use the audioContent helper to return audio files:
import { audioContent } from "fastmcp";

server.addTool({
  name: "generateSpeech",
  description: "Generate speech from text",
  parameters: z.object({
    text: z.string(),
  }),
  execute: async (args) => {
    return audioContent({
      url: "https://example.com/audio.mp3",
    });
  },
});

Combining Content Types

Return multiple content types in a single response:
import { imageContent, audioContent } from "fastmcp";

server.addTool({
  name: "multiContent",
  description: "Return multiple content types",
  execute: async () => {
    const img = await imageContent({
      url: "https://example.com/image.png",
    });
    const aud = await audioContent({
      url: "https://example.com/audio.mp3",
    });

    return {
      content: [
        { type: "text", text: "Here's your content:" },
        img,
        aud,
      ],
    };
  },
});

Tool Authorization

Control access to tools based on user authentication:
server.addTool({
  name: "admin-tool",
  description: "An admin-only tool",
  canAccess: (auth) => auth?.role === "admin",
  execute: async () => {
    return "Welcome, admin!";
  },
});
See the Authentication section for more details on authorization helpers like requireAuth, requireScopes, and requireRole.

Progress Reporting

Report progress during long-running operations:
server.addTool({
  name: "download",
  description: "Download a file",
  parameters: z.object({
    url: z.string(),
  }),
  execute: async (args, { reportProgress }) => {
    await reportProgress({ progress: 0, total: 100 });
    
    // ... perform work ...
    
    await reportProgress({ progress: 50, total: 100 });
    
    // ... more work ...
    
    await reportProgress({ progress: 100, total: 100 });
    
    return "Download complete!";
  },
});

Streaming Output

Stream partial results while the tool is executing:
server.addTool({
  name: "generateText",
  description: "Generate text incrementally",
  parameters: z.object({
    prompt: z.string(),
  }),
  annotations: {
    streamingHint: true,
    readOnlyHint: true,
  },
  execute: async (args, { streamContent }) => {
    await streamContent({ type: "text", text: "Starting generation...\n" });

    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));
    }

    // Option 1: Return void if all content was streamed
    return;

    // Option 2: Return final content to append
    // return "Generation complete!";
  },
});
Set streamingHint: true in annotations to signal that the tool uses streaming. You can return void if all content was streamed, or return a final result to append.

Tool Annotations

Provide metadata about tool behavior:
server.addTool({
  name: "fetch-content",
  description: "Fetch content from a URL",
  parameters: z.object({
    url: z.string(),
  }),
  annotations: {
    title: "Web Content Fetcher",
    readOnlyHint: true,
    openWorldHint: true,
  },
  execute: async (args) => {
    return await fetchWebpageContent(args.url);
  },
});

Available Annotations

AnnotationTypeDefaultDescription
titlestring-Human-readable title for UI display
readOnlyHintbooleanfalseTool doesn’t modify its environment
destructiveHintbooleantrueTool may perform destructive updates
idempotentHintbooleanfalseRepeated calls have no additional effect
openWorldHintbooleantrueTool may interact with external entities
streamingHintbooleanfalseTool uses streaming output

Logging

Log messages to the client during tool execution:
server.addTool({
  name: "download",
  description: "Download a file",
  parameters: z.object({
    url: z.string(),
  }),
  execute: async (args, { log }) => {
    log.info("Downloading file...", { url: args.url });
    
    // ... perform download ...
    
    log.info("Downloaded file");
    
    return "done";
  },
});
Available log methods:
  • log.debug(message, data?)
  • log.info(message, data?)
  • log.warn(message, data?)
  • log.error(message, data?)

Error Handling

Throw UserError for errors meant to be shown to the user:
import { UserError } from "fastmcp";

server.addTool({
  name: "download",
  description: "Download a file",
  parameters: z.object({
    url: z.string(),
  }),
  execute: async (args) => {
    if (args.url.startsWith("https://example.com")) {
      throw new UserError("This URL is not allowed");
    }
    
    return "done";
  },
});

Timeouts

Set a timeout for tool execution:
server.addTool({
  name: "longRunning",
  description: "A long-running operation",
  timeoutMs: 5000, // 5 seconds
  execute: async () => {
    // Will abort after 5 seconds
    await someSlowOperation();
    return "done";
  },
});

API Reference

Tool Type

type Tool<T, Params> = {
  name: string;
  description?: string;
  parameters?: Params; // Standard Schema
  annotations?: ToolAnnotations;
  canAccess?: (auth: T) => boolean;
  timeoutMs?: number;
  execute: (
    args: InferredArgs,
    context: Context<T>
  ) => Promise<string | Content | ContentResult | void>;
};

Context Type

type Context<T> = {
  session: T | undefined;
  sessionId?: string;
  requestId?: string;
  log: {
    debug: (message: string, data?: any) => void;
    info: (message: string, data?: any) => void;
    warn: (message: string, data?: any) => void;
    error: (message: string, data?: any) => void;
  };
  reportProgress: (progress: Progress) => Promise<void>;
  streamContent: (content: Content | Content[]) => Promise<void>;
  client: {
    version: string;
  };
};

Build docs developers (and LLMs) love