Tools are a core concept in the Model Context Protocol (MCP). They allow you to define functions that can be called by clients, including AI models. This guide covers everything you need to know about defining, using, and extending tools in Fast MCP.
- What are MCP Tools?
- Defining Tools
- Calling Tools From Another Tool
- Advanced Tool Features
- Best Practices
- Examples
MCP Tools are functions that can be called by clients with arguments and return results. They are defined on the server side and can be discovered and called by clients. Tools can:
- Perform calculations
- Access and modify resources
- Interact with external systems
- Call other tools
- Return structured data
Tools are particularly useful for AI models, as they provide a way for models to perform actions in the real world.
To define a tool, create a class that inherits from FastMcp::Tool:
class HelloTool < FastMcp::Tool
description "Say hello to someone"
def call(**_args)
"Hello, world!"
end
end
# Create a server
server = FastMcp::Server.new(name: 'example-server', version: '1.0.0')
# Register the tool with the server
server.register_tool(HelloTool)When defining a tool class, you can:
- Set a description using the
descriptionclass method - Define arguments using the
argumentsclass method with Dry::Schema - Implement the functionality in the
callinstance method
To define arguments for a tool, use the arguments class method with a block using Dry::Schema syntax:
class GreetTool < FastMcp::Tool
description "Greet a person"
arguments do
required(:name).filled(:string).description("Name of the person")
end
def call(name:)
"Hello, #{name}!"
end
end
# Register the tool
server.register_tool(GreetTool)The arguments method takes a block where you can define:
- Required arguments using the
requiredmethod - Optional arguments using the
optionalmethod - Types and validations for each argument
- Descriptions for each argument
Fast MCP supports the following argument types using Dry::Schema predicates:
:string: A string value:integer: An integer value:float: A floating-point number:bool: A boolean value (true/false):array: An array of values:hash: A hash/object with key-value pairs
Example with different types:
class ProcessDataTool < FastMcp::Tool
description "Process various types of data"
arguments do
required(:text).filled(:string).description("Text to process")
optional(:count).filled(:integer).description("Number of times to process")
optional(:factor).filled(:float).description("Multiplication factor")
optional(:verbose).filled(:bool).description("Whether to output verbose logs")
optional(:tags).array(:string).description("Tags to apply")
optional(:metadata).hash.description("Additional metadata")
end
def call(text:, count: 1, factor: 1.0, verbose: false, tags: [], metadata: {})
# Implementation
result = text * count
result = result * factor if factor != 1.0
if verbose
{
result: result,
tags: tags,
metadata: metadata
}
else
result
end
end
endFast MCP automatically validates arguments based on the Dry::Schema definition. If validation fails, an error is returned to the client.
You can also add custom validation in the call method:
class DivideTool < FastMcp::Tool
description "Divide two numbers"
arguments do
required(:dividend).filled(:float).description("Number to be divided")
required(:divisor).filled(:float).description("Number to divide by")
end
def call(dividend:, divisor:)
# Custom validation
raise "Cannot divide by zero" if divisor == 0
dividend / divisor
end
endYou can specify default values in the method parameters of the call method:
class RepeatTool < FastMcp::Tool
description "Repeat a string multiple times"
arguments do
required(:text).filled(:string).description("Text to repeat")
optional(:count).filled(:integer).description("Number of times to repeat")
end
def call(text:, count: 3)
text * count
end
endUsing the headers method, you can access headers passed to the tool call. This can be used to identify a user by authentication details passed in headers:
class CurrentUserTool < FastMcp::Tool
description "Gets the current user details"
def call
JSON.generate current_user
end
private
def current_user
token = headers["AUTHORIZATION"]
# Validate token
# ...
user
end
endThis can be combined with the authorize method to ensure a user is authorized before allowing them to use the tool:
class PerformAuthenticatedActionTool < FastMcp::Tool
description "Perform an action which requires an authenticated user"
arguments do
required(:item_id).filled(:integer).description('ID of item to affect')
end
authorize do |item_id:|
current_user&.is_admin? &&
get_item(item_id).user_id == current_user.id
end
def call(item_id:)
# Perform action
# ...
end
private
def current_user
# Get current user
# ...
end
def get_item(id)
# Get item
# ...
end
endYou can also implement this in a parent class and the authorization will be inherited by all children. Children may also define their own authorization - in this case, all authorization checks must pass for a caller to be allowed access to the tool.
Tools can call other tools:
class GreetTool < FastMcp::Tool
description 'Greet one person'
arguments do
required(:names).array(:string).description("Name of person to greet")
end
def call(name:)
"Hey #{name}"
end
end
class GreetMultipleTool < FastMcp::Tool
description "Greet multiple people"
arguments do
required(:names).array(:string).description("Names of people to greet")
end
def call(names:)
raise "Server not set" unless self.class.server
greet_tool = GreetTool.new
results = names.map do |name|
# Call the tool
greet_tool.call(name: name)
end
results.join("\n")
end
endTool annotations provide additional metadata about a tool's behavior, helping clients understand how to present and manage tools. These annotations are hints that describe the nature and impact of a tool.
class WebSearchTool < FastMcp::Tool
description 'Search the web for information'
annotations(
title: 'Web Search', # Human-readable title for the tool
read_only_hint: true, # Indicates the tool doesn't modify its environment
open_world_hint: true # The tool interacts with external entities
)
arguments do
required(:query).filled(:string).description('Search query')
end
def call(query:)
"Searching for: #{query}"
end
endAvailable annotations:
| Annotation | Type | Default | Description |
|---|---|---|---|
title |
string | - | A human-readable title for the tool, useful for UI display |
read_only_hint |
boolean | false | If true, indicates the tool does not modify its environment |
destructive_hint |
boolean | true | If true, the tool may perform destructive updates (only meaningful when read_only_hint is false) |
idempotent_hint |
boolean | false | If true, calling the tool repeatedly with the same arguments has no additional effect |
open_world_hint |
boolean | true | If true, the tool may interact with an "open world" of external entities |
Example with all annotations:
class DeleteFileTool < FastMcp::Tool
description 'Delete a file from the filesystem'
annotations(
title: 'Delete File',
read_only_hint: false, # This tool modifies the filesystem
destructive_hint: true, # Deleting files is destructive
idempotent_hint: true, # Deleting the same file twice has no additional effect
open_world_hint: false # Only interacts with the local filesystem
)
arguments do
required(:path).filled(:string).description('File path to delete')
end
def call(path:)
File.delete(path) if File.exist?(path)
"File deleted: #{path}"
end
endImportant: Annotations are hints and not guaranteed to provide a faithful description of tool behavior. Clients should never make security-critical decisions based solely on annotations.
Tool hidden arguments
If need be, we can register arguments that won't show up in the tools/list call but can still be used in the tool when provided. This might be useful when calling from another tool, or when the client is made aware of this argument from the context.
class AddUserTool < FastMcp::Tool
description 'Add a new user'
tool_name 'add_user'
arguments do
required(:name).filled(:string).description("User's name")
required(:email).filled(:string).description("User's email")
optional(:admin).maybe(:bool).hidden
end
def call(name:, email:, admin: nil)
# Create the new user
new_user = { name: name, email: email }
new_user[:admin] = admin if admin
new_user
end
endThe .hidden predicate takes a boolean value as argument, meaning that it can be variabilized depending on your custom logic. Useful for feature-flagging arguments.
class AddUserTool < FastMcp::Tool
description 'Add a new user'
tool_name 'add_user'
arguments do
required(:name).filled(:string).description("User's name")
required(:email).filled(:string).description("User's email")
optional(:admin).maybe(:bool).hidden(!ENV['FEATURE_FLAG'] == 'true')
end
def call(name:, email:, admin: nil)
# Create the new user
new_user = { name: name, email: email }
new_user[:admin] = admin if admin
new_user
end
endYou can organize tools into categories using instance variables or metadata:
class AddTool < FastMcp::Tool
description "Add two numbers"
class << self
attr_accessor :category
end
self.category = "Math"
arguments do
required(:a).filled(:float).description("First number")
required(:b).filled(:float).description("Second number")
end
def call(a:, b:)
a + b
end
end
class SubtractTool < FastMcp::Tool
description "Subtract two numbers"
class << self
attr_accessor :category
end
self.category = "Math"
arguments do
required(:a).filled(:float).description("First number")
required(:b).filled(:float).description("Second number")
end
def call(a:, b:)
a - b
end
endYou can add metadata to tools using class methods:
MCP specifies that we can declare metadata in the tool call result. For this, we have a _meta attr_accessor in all tools. We kept the _meta original naming to avoid collisions with arguments that could be named "metadata". It is a hash that accepts modifications and will be returned to the tool call response whenever it has been modified.
class RepeatTool < FastMcp::Tool
description "Repeat a string multiple times"
arguments do
required(:text).filled(:string).description("Text to repeat")
optional(:count).filled(:integer).description("Number of times to repeat")
end
def call(text:, count: 3)
_meta[:foo] = 'bar'
_meta[:some_key] = 'some value'
text * count
end
endYou can implement permission checks:
class AdminActionTool < FastMcp::Tool
description "Perform an admin action"
class << self
attr_accessor :required_permission
end
self.required_permission = :admin
arguments do
required(:action).filled(:string).description("Action to perform")
required(:user_role).filled(:string).description("Role of the user making the request")
end
def call(action:, user_role:)
# Check permissions
raise "Permission denied: admin role required" unless user_role == "admin"
# Perform the action
"Admin action '#{action}' performed successfully"
end
endWhen using the Rack transport, HTTP headers from tool call requests are exposed to tools via the headers method:
class MyTool < FastMcp::Tool
def call
"Host header is #{headers["HOST"]}"
end
endIt can be useful to extract authentication into modules to share functionality without having to bake logic into your tool's ancestor chain.
# This module adds a current_user method to tools which include it, and requires that the user is present
module UserAuthenticator
def self.included(tool)
tool.authorize do
not current_user.nil?
end
end
def current_user
# Get current user
# ...
end
end
# This module ensures that the THIRD_PARTY_API_KEY header is set
module ThirdPartyApiKeyRequired
def self.included(tool)
tool.authorize do
not headers['THIRD_PARTY_API_KEY'].nil?
end
end
end
class MyTool < FastMcp::Tool
# Extra authentications are executed in the order they appear in the tool.
# In this case:
# - Any authorizations from ancestor classes
# - UserAuthenticator
# - This tool's authorize call
# - ThirdParyApiKeyRequired
include UserAuthenticator
authorize do
# My custom auth for this tool
# ...
end
include ThirdPartyApiKeyRequired
endHere are some best practices for working with MCP tools:
- Use Clear Names: Give your tools clear, descriptive names that indicate their purpose.
- Provide Good Descriptions: Write detailed descriptions for tools and their arguments.
- Validate Inputs: Use the schema validation to ensure inputs are correct before processing.
- Handle Errors Gracefully: Catch and handle errors properly, providing clear error messages.
- Return Structured Data: Return structured data when appropriate, especially for complex results.
- Test Your Tools: Write tests for your tools to ensure they work correctly.
- Document Usage: Document how to use your tools, including examples.
- Keep Tools Focused: Each tool should do one thing well, rather than trying to do too much.
Here's a more complex example of a tool that interacts with resources:
class IncrementCounterTool < FastMcp::Tool
description "Increment a counter resource"
# Class variable to hold server instance
@server = nil
# Class methods to get and set server instance
class << self
attr_accessor :server
end
arguments do
optional(:amount).filled(:integer).description("Amount to increment by")
end
def call(amount: 1)
raise "Server not set" unless self.class.server
# Get the counter resource
counter_resource = self.class.server.resources["counter"]
raise "Counter resource not found" unless counter_resource
# Parse the current value
current_value = counter_resource.content.to_i
# Increment the counter
new_value = current_value + amount
# Update the resource
counter_resource.update_content(new_value.to_s)
# Return the new value
{ previous_value: current_value, new_value: new_value, amount: amount }
end
end
# Set the server reference
IncrementCounterTool.server = server
# Register the tool
server.register_tool(IncrementCounterTool)This tool increments a counter resource by a specified amount (or by 1 by default) and returns the previous and new values.