Skip to main content

Overview

The Model Context Protocol (MCP) enables agents to interact with external tools, resources, and data sources. Fast Agent provides seamless integration with MCP servers.

Built-in MCP Servers

Fast Agent comes with several built-in MCP servers you can use immediately:
  • fetch - Download and read web content
  • filesystem - Read and write local files
  • time - Get current time in different timezones
  • interpreter - Execute Python code with installed packages
  • brave-search - Search the web using Brave Search API
  • github - Interact with GitHub repositories
import asyncio
from fast_agent import FastAgent

fast = FastAgent("MCP Demo")

@fast.agent(
    name="researcher",
    instruction="You are a research assistant that can fetch web content and search.",
    servers=["fetch", "brave-search"],  # Connect to multiple servers
)
async def main():
    async with fast.run() as agent:
        await agent.researcher(
            "Research the latest developments in AI agents and summarize the key trends"
        )

if __name__ == "__main__":
    asyncio.run(main())

Building a Custom MCP Server

Create custom MCP servers using FastMCP:
examples/mcp/a3_styling_server.py
from __future__ import annotations

from typing import Any
from mcp.server.fastmcp import Context, FastMCP
from pydantic import BaseModel

app = FastMCP(name="A3 Styling Demo")

class StructuredReport(BaseModel):
    title: str
    items: list[str]
    metadata: dict[str, Any]

@app.tool(
    name="a3_structured_report",
    description="Return structured and unstructured content for A3 tool result styling.",
    structured_output=True,
)
def a3_structured_report(topic: str, count: int = 3) -> StructuredReport:
    items = [f"{topic} item {index + 1}" for index in range(count)]
    return StructuredReport(
        title=f"Structured report for {topic}",
        items=items,
        metadata={
            "topic": topic,
            "count": count,
            "note": "This payload should show structured content in A3 tool results.",
        },
    )

@app.tool(
    name="a3_unstructured_echo",
    description="Return a plain-text response to show non-structured tool styling.",
)
def a3_unstructured_echo(message: str) -> str:
    return f"Echo: {message}"

if __name__ == "__main__":
    app.run()
Run the server:
uv run python examples/mcp/a3_styling_server.py
Use structured_output=True to return structured data that Fast Agent can display in a formatted way.

Dynamic Tool Registration

Add tools dynamically and notify clients:
EXTRA_TOOL_NAME = "a3_extra_tool"
_EXTRA_TOOL_ADDED = False

def _extra_tool(note: str = "Tool list update confirmed") -> str:
    return f"Extra tool active: {note}"

@app.tool(
    name="a3_trigger_tool_update",
    description="Add a new tool and emit a tool list changed notification.",
)
async def a3_trigger_tool_update(context: Context) -> str:
    global _EXTRA_TOOL_ADDED
    
    if not _EXTRA_TOOL_ADDED:
        context.fastmcp.add_tool(
            _extra_tool,
            name=EXTRA_TOOL_NAME,
            description="Extra tool registered after tool list update.",
        )
        _EXTRA_TOOL_ADDED = True
    
    await context.request_context.session.send_tool_list_changed()
    return "Tool list change notification sent. Refresh tools to see updates."

MCP Prompts

Create reusable prompt templates:
@app.prompt("a3_daily_brief")
def a3_daily_brief(project: str, focus: str = "status") -> str:
    """Prompt with arguments to exercise /prompt selection and argument collection."""
    return (
        "You are an A3-style briefing assistant. "
        f"Provide a {focus} update for project '{project}' with three bullets."
    )

@app.prompt("a3_review")
def a3_review(area: str = "UI") -> str:
    """Simple prompt for /prompt listing and selection UI."""
    return (
        "You are reviewing the A3 display style for the following area: "
        f"{area}. Provide a short critique and a next-step checklist."
    )
Use prompts in your agent:
fast-agent go
/prompt a3_daily_brief project=Fast-Agent focus=features

Elicitations (Interactive Forms)

Create interactive forms for collecting structured input:
examples/mcp/elicitations/elicitation_game_server.py
import logging
import random
from mcp import ReadResourceResult
from mcp.server.elicitation import (
    AcceptedElicitation,
    CancelledElicitation,
    DeclinedElicitation,
)
from mcp.server.fastmcp import FastMCP
from mcp.types import TextResourceContents
from pydantic import AnyUrl, BaseModel, Field

mcp = FastMCP("Game Character Creation Server", log_level="INFO")

@mcp.resource(uri="elicitation://game-character")
async def game_character() -> ReadResourceResult:
    """Fun game character creation form for the whimsical example."""
    
    class GameCharacter(BaseModel):
        character_name: str = Field(description="Name your character", min_length=2, max_length=30)
        character_class: str = Field(
            description="Choose your class",
            json_schema_extra={
                "enum": ["warrior", "mage", "rogue", "ranger", "paladin", "bard"],
                "enumNames": [
                    "⚔️ Warrior",
                    "🔮 Mage",
                    "🗡️ Rogue",
                    "🏹 Ranger",
                    "🛡️ Paladin",
                    "🎵 Bard",
                ],
            },
        )
        strength: int = Field(description="Strength (3-18)", ge=3, le=18, default=10)
        intelligence: int = Field(description="Intelligence (3-18)", ge=3, le=18, default=10)
        dexterity: int = Field(description="Dexterity (3-18)", ge=3, le=18, default=10)
        charisma: int = Field(description="Charisma (3-18)", ge=3, le=18, default=10)
        lucky_dice: bool = Field(False, description="Roll for a lucky bonus?")
    
    result = await mcp.get_context().elicit("🎮 Create Your Game Character!", schema=GameCharacter)
    
    match result:
        case AcceptedElicitation(data=data):
            lines = [
                f"🎭 Character Created: {data.character_name}",
                f"Class: {data.character_class.title()}",
                f"Stats: STR:{data.strength} INT:{data.intelligence} DEX:{data.dexterity} CHA:{data.charisma}",
            ]
            
            if data.lucky_dice:
                dice_roll = random.randint(1, 20)
                if dice_roll >= 15:
                    bonus = random.choice([
                        "🎁 Lucky! +2 to all stats!",
                        "🌟 Critical! Found a magic item!",
                        "💰 Jackpot! +100 gold!",
                    ])
                    lines.append(f"🎲 Dice Roll: {dice_roll} - {bonus}")
                else:
                    lines.append(f"🎲 Dice Roll: {dice_roll} - No bonus this time!")
            
            response = "\n".join(lines)
        case DeclinedElicitation():
            response = "Character creation declined - returning to menu"
        case CancelledElicitation():
            response = "Character creation cancelled"
    
    return ReadResourceResult(
        contents=[
            TextResourceContents(
                mimeType="text/plain", uri=AnyUrl("elicitation://game-character"), text=response
            )
        ]
    )

if __name__ == "__main__":
    mcp.run()
Elicitations provide a way for agents to collect structured user input through interactive forms.

Tool-Based Elicitation

Trigger elicitations from tools:
@mcp.tool()
async def roll_new_character(campaign_name: str = "Adventure") -> str:
    """
    Roll a new character for your campaign.
    
    Args:
        campaign_name: The name of the campaign
    
    Returns:
        Character details or status message
    """
    
    class GameCharacter(BaseModel):
        character_name: str = Field(description="Name your character", min_length=2, max_length=30)
        character_class: str = Field(
            description="Choose your class",
            json_schema_extra={
                "enum": ["warrior", "mage", "rogue", "ranger", "paladin", "bard"],
            },
        )
        strength: int = Field(description="Strength (3-18)", ge=3, le=18, default=10)
    
    result = await mcp.get_context().elicit(
        f"🎮 Create Character for {campaign_name}!", schema=GameCharacter
    )
    
    match result:
        case AcceptedElicitation(data=data):
            response = f"🎭 {data.character_name} the {data.character_class.title()} joins {campaign_name}!\n"
            response += f"Stats: STR:{data.strength}"
            return response
        case DeclinedElicitation():
            return f"Character creation for {campaign_name} was declined"
        case CancelledElicitation():
            return f"Character creation for {campaign_name} was cancelled"

Configuring MCP Servers

Add custom MCP servers to your configuration:
fastagent.config.yaml
mcp:
  servers:
    custom-server:
      command: uv
      args:
        - run
        - python
        - path/to/my_server.py
      env:
        API_KEY: "${API_KEY}"  # Environment variables
Use in your agent:
@fast.agent(
    name="custom_agent",
    instruction="You can use my custom tools.",
    servers=["custom-server"],
)
async def main():
    async with fast.run() as agent:
        await agent.interactive()

Testing MCP Servers

Test your MCP server standalone:
# Run the server
uv run python my_server.py

# In another terminal, test with fast-agent
fast-agent go --servers my-server
Use the --servers flag to test specific MCP servers without modifying your configuration.

Best Practices

Always handle errors gracefully in your tools:
@app.tool()
def risky_operation(value: str) -> str:
    try:
        # Do something that might fail
        result = process(value)
        return f"Success: {result}"
    except ValueError as e:
        return f"Error: Invalid value - {e}"
    except Exception as e:
        return f"Error: {e}"
Use Pydantic models for structured data:
class SearchResult(BaseModel):
    title: str
    url: str
    snippet: str
    relevance_score: float

@app.tool(structured_output=True)
def search(query: str) -> list[SearchResult]:
    # Return structured data
    return [SearchResult(...)]
Use proper logging for debugging:
import logging

logger = logging.getLogger(__name__)

@app.tool()
def complex_operation(data: str) -> str:
    logger.info(f"Processing: {data}")
    result = process(data)
    logger.debug(f"Result: {result}")
    return result
Use context managers for resource management:
@app.tool()
async def process_file(path: str) -> str:
    async with aiofiles.open(path, 'r') as f:
        content = await f.read()
        return process(content)

Next Steps

Advanced Patterns

Explore hooks, RAG, and production patterns

MCP Documentation

Learn more about the Model Context Protocol

Configuration

Configure MCP servers in detail

Built-in Servers

Explore all built-in MCP servers

Build docs developers (and LLMs) love