MCP Framework
Tools

Tools Overview

Building tools with MCP Framework - create custom tools that extend AI model capabilities

Building Tools with MCP Framework

Power of Tools

Tools are the powerhouse of your MCP server - they let AI models interact with external services, process data, and perform complex operations with type safety!

What is a Tool?

A tool is a MCP class that defines:

  • What inputs it accepts
  • What it does with those inputs
  • What it returns to the AI model

Here's a simple example:

import { MCPTool } from "mcp-framework";
import { z } from "zod";

interface GreetingInput {
  name: string;
  language: string;
}

class GreetingTool extends MCPTool<GreetingInput> {
  name = "greeting";
  description = "Generate a greeting in different languages";

  schema = {
    name: {
      type: z.string(),
      description: "Name to greet",
    },
    language: {
      type: z.enum(["en", "es", "fr"]),
      description: "Language code (en, es, fr)",
    },
  };

  async execute({ name, language }) {
    const greetings = {
      en: `Hello ${name}!`,
      es: `¡Hola ${name}!`,
      fr: `Bonjour ${name}!`,
    };
    return greetings[language];
  }
}

Creating Tools

Using the CLI

The fastest way to create a new tool:

mcp add tool my-tool

This generates a tool template in src/tools/MyTool.ts.

Manual Creation

  1. Create a new TypeScript file in src/tools/
  2. Extend the MCPTool class
  3. Define your interface and implementation

Tool Architecture

Every tool has three main parts:

1. Input Schema

schema = {
  email: {
    type: z.string().email(),
    description: "User's email address",
  },
  count: {
    type: z.number().min(1),
    description: "Number of items to process",
  },
};

2. Metadata

name = "email-sender";
description = "Sends emails to specified addresses";

3. Execution Logic

async execute(input: MyInput) {
  // Your tool's core functionality
  return result;
}

Type Safety

Type Safety First

MCP Framework leverages TypeScript and Zod to provide end-to-end type safety!

interface DataInput {
  userId: number;
  fields: string[];
}

class DataTool extends MCPTool<DataInput> {
  schema = {
    userId: {
      type: z.number(),
      description: "User ID to fetch data for",
    },
    fields: {
      type: z.array(z.string()),
      description: "Fields to include in response",
    },
  };
}

Error Handling

Tools should handle errors gracefully:

async execute(input: MyInput) {
  try {
    const result = await this.processData(input);
    return result;
  } catch (error) {
    if (error.code === 'NETWORK_ERROR') {
      throw new Error('Unable to reach external service');
    }
    throw new Error(`Operation failed: ${error.message}`);
  }
}

Title and Icons

Tools can set a title for human-readable display and icons for client UI rendering. The title is separate from the programmatic name and is intended for display in tool pickers, dashboards, and other client interfaces.

import { MCPTool } from "mcp-framework";
import { z } from "zod";

class WeatherTool extends MCPTool {
  name = "get_weather";
  title = "Weather Information";  // Human-readable display name
  description = "Get current weather for a location";
  icons = [{ src: "https://example.com/weather.png", mimeType: "image/png", sizes: ["48x48"] }];

  schema = {
    location: {
      type: z.string(),
      description: "City or address to get weather for",
    },
  };

  async execute({ location }) {
    // ...
  }
}

The icons property accepts an array of icon objects, each with:

  • src — URL to the icon image
  • mimeType — MIME type of the image (e.g., "image/png", "image/svg+xml")
  • sizes — Array of size strings (e.g., ["48x48", "96x96"])

Tool Annotations

Annotations provide behavioral hints that help clients make UX decisions about your tools. For example, a client might auto-approve tools marked as read-only, or show a confirmation dialog for destructive tools.

class DatabaseTool extends MCPTool {
  name = "query_db";
  description = "Query the database";
  annotations = {
    readOnlyHint: true,      // Tool doesn't modify state
    destructiveHint: false,   // Tool is not destructive
    idempotentHint: true,     // Safe to retry
    openWorldHint: false,     // Doesn't access external systems
  };

  schema = {
    query: {
      type: z.string(),
      description: "SQL query to execute",
    },
  };

  async execute({ query }) {
    // ...
  }
}

Annotations Are Hints

Annotations are advisory only and are NOT enforced by the framework or the MCP protocol. Clients may use them to improve the user experience, but they should not be relied upon for security or correctness guarantees.

Available annotation properties:

PropertyTypeDefaultDescription
readOnlyHintbooleanfalseTool does not modify any state
destructiveHintbooleantrueTool may perform destructive operations
idempotentHintbooleanfalseCalling the tool multiple times with the same input has the same effect
openWorldHintbooleantrueTool may interact with external systems beyond its host

New Content Types

In addition to returning plain text, tools can now return audio content, resource links, and embedded resources.

Audio Content

Return audio data as base64-encoded content:

async execute(input) {
  const audioData = await generateSpeech(input.text);
  return { type: 'audio', data: audioData, mimeType: 'audio/wav' };
}

Return a URI reference to an existing resource. The client can then fetch it independently:

async execute(input) {
  return {
    type: 'resource_link',
    uri: 'file:///project/src/main.rs',
    name: 'main.rs',
    mimeType: 'text/x-rust'
  };
}

Embedded Resources

Return a resource with inline content included directly in the response:

async execute(input) {
  return {
    type: 'resource',
    resource: {
      uri: 'file:///README.md',
      mimeType: 'text/markdown',
      text: '# Hello'
    }
  };
}

Content Annotations

All content types support optional annotations that indicate the intended audience, priority, and freshness of the content:

async execute(input) {
  return {
    type: 'text',
    text: 'Result for user',
    annotations: {
      audience: ['user'],           // Who this is for: 'user', 'assistant', or both
      priority: 0.9,                // 0.0 (optional) to 1.0 (critical)
      lastModified: '2025-01-12T15:00:58Z'
    }
  };
}
PropertyTypeDescription
audiencestring[]Who the content is intended for. Values: 'user', 'assistant', or both. Omit to indicate both.
prioritynumberImportance from 0.0 (background/optional) to 1.0 (critical). Used by clients to order or filter content.
lastModifiedstringISO 8601 timestamp of when the content was last modified.

Best Practices

Pro Tips

Following these practices will make your tools more reliable and maintainable!

  1. Clear Names

    name = "fetch-user-data"; // Good
    name = "fud"; // Bad
  2. Detailed Descriptions

    Descriptions are also read by the LLMs - so make sure to make them detailed

    description =
      "Fetches user data including profile, preferences, and settings";
  3. Descriptive Input Validation

    schema = {
      age: {
        type: z.number().min(0).max(150),
        description: "User's age (0-150)",
      },
    };

Next Steps