Building a Simple AI Agent Using FastAPI, LangGraph, and MCP

FastAPILangGraphAI Agents Model Context ProtocolPythonBackend Engineering
Building a Simple AI Agent Using FastAPI, LangGraph, and MCP

Modern AI agents are no longer just single LLM calls.

Real-world agents need:

  • Tool access
  • Memory
  • Workflow orchestration
  • Prompt management
  • Clean APIs

In this article, we'll build a simple but production-ready AI agent using FastAPI, LangGraph, and MCP (Model Context Protocol).

This guide is aimed at backend engineers who want both a solid mental model and a practical implementation.

Tech Stack Overview

We'll use the following components:

  • FastAPI – API layer
  • FastMCP – Tool and prompt server (MCP implementation)
  • LangGraph – Agent workflow orchestration
  • LangChain MCP Adapters – Bridge between MCP and LangGraph
  • Redis – Event and memory storage

By the end, the agent will be able to:

  • Call external tools such as Wikipedia and REST Countries
  • Load prompts dynamically from MCP
  • Execute a LangGraph-based reasoning workflow
  • Persist conversations and events

High-Level Architecture

The architecture is intentionally modular.

Client
   │
   ▼
FastAPI
   │
   ▼
MCP Client (HTTP)
   │
   ▼
FastMCP Server
   ├── Tools
   └── Prompts
   │
   ▼
LangGraph Agent

Key Design Idea

Each component has a single responsibility:

  • MCP acts as the tool and prompt server
  • LangGraph acts as the agent brain
  • FastAPI exposes the agent as an HTTP service

This separation keeps the system maintainable, scalable, and easy to evolve.

Step 1: FastAPI as the Agent Gateway

FastAPI serves as the entry point for all client requests.

It also hosts the MCP server within the same process.

from fastapi import FastAPI
from mcp_server.server import mcp_app
from workflows.graph import create_graph
from langchain_mcp_adapters.client import MultiServerMCPClient

app = FastAPI(lifespan=mcp_app.lifespan)

app.mount("/agent", mcp_app)

Mounting MCP inside FastAPI allows both services to run together while remaining logically separated.


MCP Client Configuration

FastAPI communicates with MCP using an HTTP-based MCP client.

client = MultiServerMCPClient(
    {
        "agent": {
            "transport": "http",
            "url": "http://localhost:8000/agent/mcp",
        },
    }
)

The client is responsible for:

  • Discovering available tools
  • Loading prompts
  • Establishing MCP sessions

All of this happens dynamically at runtime.

Step 2: Creating the Workflow Endpoint

The workflow endpoint is where agent execution begins.

Its responsibilities are:

  1. Open an MCP session
  2. Build the LangGraph agent
  3. Execute the workflow
  4. Return the final response
@app.post("/workflow")
async def run_workflow(message: str):

    config = {
        "configurable": {
            "thread_id": "001"
        }
    }

    async with client.session("agent") as session:

        agent = await create_graph(
            session=session
        )

        response = await agent.ainvoke(
            {"messages": message},
            config=config
        )

        return response["messages"][-1].content


Why thread_id Matters

The thread_id enables:

  • Conversation memory
  • Checkpointing
  • Stateful agent execution

Without it, every request would behave like a completely new conversation.

Step 3: Defining MCP Tools with FastMCP

FastMCP makes tool creation extremely simple using decorators.

These tools automatically become discoverable by LangGraph.

Wikipedia Tool

@mcp.tool(
    name="global_news",
    description="Get global news from Wikipedia"
)
async def global_news(query: str):
    return wikipedia.summary(query)
Country Details Tool
@mcp.tool(
    name="get_countries_details",
    description="Get details of a country"
)
async def get_countries_details(country_name: str):

    async with httpx.AsyncClient(
        timeout=15.0
    ) as client:

        response = await client.get(
            f"https://restcountries.com/v3.1/name/{country_name}?fullText=true"
        )

        response.raise_for_status()

        return response.json()

Currency Tool

@mcp.tool(
    name="get_currency",
    description="Get details of a currency"
)
async def get_currency(currency_code: str):

    async with httpx.AsyncClient(
        timeout=15.0
    ) as client:

        response = await client.get(
            f"https://restcountries.com/v3.1/currency/{currency_code}"
        )

        response.raise_for_status()

        return response.json()


Why MCP Tools Are Powerful

Once registered:

  • The LLM can discover them dynamically
  • No manual wiring is required
  • Tools can be shared across multiple agents
  • New tools can be added independently of agent logic

Step 4: Managing Prompts Through MCP

Rather than hardcoding prompts inside the agent, MCP can serve prompts as reusable resources.

@mcp.prompt
async def common_prompt() -> str:
    return """
    You are a helpful assistant.
    Answer the question based on the tools provided.
    """

Benefits of MCP Prompts

Centralized Prompt Management

Update prompts in one location.

Runtime Updates

Modify instructions without redeploying agents.

Reusability

Multiple agents can consume the same prompts.

This becomes especially valuable as organizations scale their agent ecosystem.

Step 5: Adding Redis-Based Event Storage

To support memory and event persistence, we configure Redis as the MCP event store.

from fastmcp.server.event_store import EventStore
from key_value.aio.stores.redis import RedisStore

redis_store = RedisStore(
    url="redis://localhost:6379"
)

event_store = EventStore(
    storage=redis_store,
    max_events_per_stream=100,
    ttl=3600,
)

Creating the MCP Application

def create_app():

    register_tools(mcp)
    register_prompts(mcp)

    return mcp.http_app(
        event_store=event_store,
        path="/mcp"
    )

mcp_app = create_app()

At this point MCP is responsible for:

  • Tool registration
  • Prompt registration
  • Event persistence
  • Session management

Step 6: Constructing the LangGraph Agent

LangGraph orchestrates the agent's reasoning process.

The first step is loading tools and prompts from MCP.

tools = await load_mcp_tools(session)

system_prompt = await load_mcp_prompt(
    session=session,
    name="common_prompt"
)

Building the Prompt Template

prompt_template = ChatPromptTemplate.from_messages(
    [
        ("system", system_prompt[0].content),
        MessagesPlaceholder("messages")
    ]
)

Binding Tools to the LLM

llm_with_tool = llm.bind_tools(tools)

chat_llm = prompt_template | llm_with_tool

This enables the model to decide when tools should be invoked during reasoning.

Step 7: Defining the LangGraph Workflow

The workflow controls how the agent alternates between reasoning and tool execution.

graph = StateGraph(EnrichmentState)

graph.add_node(
    "chat_node",
    chat_node
)

graph.add_node(
    "tool_node",
    ToolNode(tools=tools)
)

graph.add_edge(
    START,
    "chat_node"
)

graph.add_conditional_edges(
    "chat_node",
    tools_condition,
    {
        "tools": "tool_node",
        "__end__": END
    }
)

graph.add_edge(
    "tool_node",
    "chat_node"
)

graph = graph.compile(
    checkpointer=MemorySaver()
)

Understanding the Agent Loop

The workflow behaves as follows:

User Input
     │
     ▼
 Chat Node
     │
     ▼
Need Tool?
 ├── No ──► End
 │
 ▼
Tool Node
 │
 ▼
Chat Node
 │
 ▼
End

Step 1

The LLM reasons about the user's request.

Step 2

If a tool is needed, LangGraph routes execution to the tool node.

Step 3

The tool executes and returns results.

Step 4

Results are fed back into the LLM.

Step 5

The loop continues until no further tools are required.

This is what makes it a true agent workflow rather than a simple one-shot LLM call.


Final Result

At the end of this setup, you have:

✅ FastAPI-powered agent APIs

✅ MCP-based tool and prompt management

✅ LangGraph workflow orchestration

✅ Redis-backed memory and event persistence

✅ Dynamic tool discovery

✅ Stateful conversations

✅ Clear separation of concerns


Why This Architecture Works

Each layer focuses on a single responsibility:

LayerResponsibilityFastAPIAPI GatewayMCPTools, Prompts, MemoryLangGraphAgent WorkflowRedisPersistenceLLMReasoning

Because these responsibilities are decoupled, the system remains maintainable as complexity grows.

You can add:

  • More tools
  • Additional prompts
  • Multiple agent workflows
  • Long-term memory
  • Human-in-the-loop approval flows

without significantly changing the core architecture.

Conclusion

Building production-ready AI agents requires more than just calling an LLM.

By combining:

  • FastAPI for APIs
  • FastMCP for tools and prompts
  • LangGraph for orchestration
  • Redis for persistence

you get a flexible architecture that cleanly separates reasoning, tool execution, memory, and API concerns.

As your agents become more sophisticated, this modular approach scales far better than embedding everything directly inside a single application or prompt.