DEV Community

Cover image for Build Your Own AI Agent with MCP: A Developer's Guide to Model Context Protocol
Tamoghna
Tamoghna

Posted on

Build Your Own AI Agent with MCP: A Developer's Guide to Model Context Protocol

Build Your Own AI Agent with MCP: A Developer's Guide to Model Context Protocol

Ever wondered how to give your AI assistant access to real-time data, databases, or custom tools? The Model Context Protocol (MCP) is your gateway to building truly intelligent agents that can interact with the world beyond their training data.

What is MCP and Why Should You Care?

If you've been working with AI assistants like Claude, ChatGPT, or local models, you've probably hit the same wall I did: they're incredibly smart, but completely isolated from your actual data and tools.

Imagine if your AI could:

  • Query your database in real-time
  • Read from your knowledge base
  • Execute custom business logic
  • Access live APIs and services

That's exactly what the Model Context Protocol (MCP) enables. Think of it as a standardized way for AI models to communicate with external tools and data sources, similar to how REST APIs work for web services.

MCP was developed by Anthropic and is quickly becoming the standard for AI-tool integration. Instead of cramming everything into prompts or fine-tuning models, you can build specialized servers that your AI can call upon when needed.

The Architecture: Servers, Clients, and Protocols

Before we dive into code, let's understand the key components:

πŸ–₯️ MCP Server

Your custom server that provides tools and data to AI models. It's like a waiter that knows exactly what's available and how to get it.

πŸ€– MCP Client

The bridge between your AI model (OpenAI, Claude, etc.) and your MCP server. It translates between AI function calls and MCP protocol.

πŸ“‘ Transport Protocols

MCP supports multiple ways for clients and servers to communicate:

  • stdio (Standard Input/Output): Great for local development and simple setups
  • SSE (Server-Sent Events): Perfect for web applications and real-time updates
  • HTTP: For traditional request-response patterns

For this tutorial, we'll focus on stdio since it's the most straightforward to get started with.

Building Your First MCP Server

Let's build a knowledge base server that can answer questions from a structured dataset. Here's the complete server implementation:

#!/usr/bin/env python3
import asyncio
import json
import os
from mcp.server import Server
import mcp.server.stdio
import mcp.types as types

# Create server instance
server = Server("knowledge-base-server")

@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
    """Define what tools your server provides"""
    return [
        types.Tool(
            name="get_knowledge_base",
            description="Retrieve information from the knowledge base",
            inputSchema={
                "type": "object",
                "properties": {},
                "required": [],
            },
        ),
    ]

@server.call_tool()
async def handle_call_tool(name: str, arguments: dict | None):
    """Handle tool execution requests"""
    if name == "get_knowledge_base":
        try:
            kb_path = os.path.join("data", "kb.json")

            with open(kb_path, 'r') as kb_file:
                kb_data = json.load(kb_file)

                # Format the knowledge base nicely
                kb_text = "Knowledge Base Contents:\n\n"
                for i, item in enumerate(kb_data, 1):
                    question = item.get("question", "Unknown")
                    answer = item.get("answer", "Unknown")
                    kb_text += f"Q{i}: {question}\n"
                    kb_text += f"A{i}: {answer}\n\n"

                return [types.TextContent(type="text", text=kb_text)]

        except Exception as e:
            return [types.TextContent(
                type="text", 
                text=f"Error accessing knowledge base: {str(e)}"
            )]
    else:
        raise ValueError(f"Unknown tool: {name}")

async def main():
    """Run the server using stdio transport"""
    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
        await server.run(read_stream, write_stream)

if __name__ == "__main__":
    asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode

Sample Knowledge Base (data/kb.json)

[
  {
    "question": "What is MCP?",
    "answer": "MCP (Model Context Protocol) is a protocol for AI assistants to communicate with external data sources and tools."
  },
  {
    "question": "What transport protocols are supported?",
    "answer": "MCP supports stdio, SSE (Server-Sent Events), and HTTP transport protocols."
  },
  {
    "question": "What is the default transport protocol for MCP?",
    "answer": "The default transport protocol for MCP is stdio (standard input/output)."
  }
]
Enter fullscreen mode Exit fullscreen mode

Creating the MCP Client

Now let's build a client that connects your AI model to the MCP server:

import asyncio
import json
from contextlib import AsyncExitStack
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from openai import AsyncOpenAI

class MCPOpenAIClient:
    def __init__(self, model="gpt-4o"):
        self.session = None
        self.exit_stack = AsyncExitStack()
        self.model = model
        self.openai_client = AsyncOpenAI()

    async def connect_to_server(self, server_script_path="server.py"):
        """Connect to the MCP server"""
        server_params = StdioServerParameters(
            command="python",
            args=[server_script_path],
        )

        # Establish connection
        stdio_transport = await self.exit_stack.enter_async_context(
            stdio_client(server_params)
        )
        stdio, write = stdio_transport

        self.session = await self.exit_stack.enter_async_context(
            ClientSession(stdio, write)
        )

        await self.session.initialize()
        print("βœ… Connected to MCP server!")

    async def get_mcp_tools(self):
        """Convert MCP tools to OpenAI function format"""
        tools_result = await self.session.list_tools()
        return [
            {
                "type": "function",
                "function": {
                    "name": tool.name,
                    "description": tool.description,
                    "parameters": tool.inputSchema,
                },
            }
            for tool in tools_result.tools
        ]

    async def process_query(self, query: str) -> str:
        """Process a query using AI + MCP tools"""
        tools = await self.get_mcp_tools()

        # First AI call with tools available
        response = await self.openai_client.chat.completions.create(
            model=self.model,
            messages=[
                {"role": "system", "content": "You have access to a knowledge base. Use it to answer questions accurately."},
                {"role": "user", "content": query}
            ],
            tools=tools,
            tool_choice="auto",
        )

        assistant_message = response.choices[0].message

        # Handle tool calls
        if assistant_message.tool_calls:
            messages = [
                {"role": "user", "content": query},
                assistant_message,
            ]

            # Execute each tool call
            for tool_call in assistant_message.tool_calls:
                result = await self.session.call_tool(
                    tool_call.function.name,
                    arguments=json.loads(tool_call.function.arguments),
                )

                messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": result.content[0].text,
                })

            # Get final response with tool results
            final_response = await self.openai_client.chat.completions.create(
                model=self.model,
                messages=messages,
            )

            return final_response.choices[0].message.content

        return assistant_message.content
Enter fullscreen mode Exit fullscreen mode

Putting It All Together

Here's how to use your new AI agent:

async def main():
    client = MCPOpenAIClient()

    try:
        # Connect to your MCP server
        await client.connect_to_server("server.py")

        # Test queries
        queries = [
            "What is MCP?",
            "What transport protocols are supported?",
            "How does this knowledge base work?"
        ]

        for query in queries:
            print(f"\nπŸ€” Question: {query}")
            response = await client.process_query(query)
            print(f"πŸ€– Answer: {response}")

    finally:
        await client.cleanup()

if __name__ == "__main__":
    asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode

Understanding Transport Protocols

stdio (What We Used)

  • Pros: Simple, great for development, works locally
  • Cons: Not suitable for web applications
  • Use case: Local AI agents, development, CLI tools
# Server runs as a subprocess, communicates via stdin/stdout
server_params = StdioServerParameters(
    command="python",
    args=["server.py"],
)
Enter fullscreen mode Exit fullscreen mode

SSE (Server-Sent Events)

  • Pros: Web-friendly, real-time updates, HTTP-based
  • Cons: More complex setup
  • Use case: Web applications, dashboards, real-time systems
# Server runs as web service, client connects via HTTP
server_params = SSEServerParameters(
    url="http://localhost:8000/sse",
)
Enter fullscreen mode Exit fullscreen mode

Why This Approach is Powerful

Traditional AI integration often looks like this:

  1. Fetch data in your application
  2. Stuff it into prompts
  3. Send to AI
  4. Parse response
  5. Hope for the best

With MCP, your workflow becomes:

  1. AI identifies what information it needs
  2. AI calls appropriate tools via MCP
  3. MCP server provides fresh, structured data
  4. AI processes and responds

This means:

  • βœ… Always fresh data (no stale embeddings)
  • βœ… Structured interactions (no prompt injection)
  • βœ… Scalable architecture (add tools without retraining)
  • βœ… Reusable components (one server, many AI clients)

Tips and Gotchas

🎯 Best Practices

  1. Start Simple: Begin with stdio transport and basic tools
  2. Error Handling: Always wrap MCP calls in try-catch blocks
  3. Tool Design: Make tools focused and well-documented
  4. Resource Cleanup: Always clean up connections properly

⚠️ Common Pitfalls

  1. Blocking Operations: Use async/await throughout your server
  2. Large Payloads: MCP isn't meant for transferring huge datasets
  3. Security: Validate all inputs in your tool handlers
  4. Connection Management: Handle disconnections gracefully
# Good: Proper error handling
try:
    result = await self.session.call_tool(tool_name, args)
    return result.content[0].text
except Exception as e:
    return f"Tool error: {str(e)}"

# Bad: No error handling
result = await self.session.call_tool(tool_name, args)
return result.content[0].text
Enter fullscreen mode Exit fullscreen mode

Project Structure for Success

Here's how I organize my MCP projects:

my-mcp-agent/
β”œβ”€β”€ server.py              # MCP server implementation
β”œβ”€β”€ client.py              # AI client with MCP integration
β”œβ”€β”€ data/
β”‚   β”œβ”€β”€ kb.json           # Knowledge base
β”‚   └── config.yaml       # Server configuration
β”œβ”€β”€ tools/
β”‚   β”œβ”€β”€ __init__.py
β”‚   β”œβ”€β”€ knowledge_base.py # Tool implementations
β”‚   └── calculator.py     # More tools
β”œβ”€β”€ requirements.txt       # Dependencies
β”œβ”€β”€ .env                  # API keys (don't commit!)
└── README.md             # Documentation
Enter fullscreen mode Exit fullscreen mode

Going Further: Advanced Ideas

Once you've mastered the basics, consider these extensions:

πŸ”„ Multi-Tool Servers

@server.list_tools()
async def handle_list_tools():
    return [
        types.Tool(name="search_database", ...),
        types.Tool(name="send_email", ...),
        types.Tool(name="generate_report", ...),
    ]
Enter fullscreen mode Exit fullscreen mode

🌐 Web-Based Agents

Switch to SSE transport for browser-based AI agents:

# Instead of stdio, use SSE for web apps
from mcp.client.sse import sse_client

async with sse_client(sse_params) as transport:
    # Your web-based AI agent
Enter fullscreen mode Exit fullscreen mode

πŸ”— Tool Chaining

Let your AI call multiple tools in sequence:

# AI can call get_user_data, then send_notification, then log_action
# All in one conversation turn!
Enter fullscreen mode Exit fullscreen mode

Essential Resources

Wrapping Up

MCP represents a fundamental shift in how we build AI applications. Instead of cramming everything into prompts or fine-tuning models, we can build modular, reusable tools that any AI can leverage.

The knowledge base server we built today is just the beginning. Imagine servers that:

  • Query your databases in real-time
  • Interact with APIs and web services
  • Execute business logic and workflows
  • Provide specialized domain knowledge

The possibilities are endless, and the barrier to entry has never been lower.

What will you build with MCP? Drop a comment below with your ideas – I'd love to see what the community creates!


Connect & Explore More

If you found this tutorial helpful, I'd love to connect! Here are some ways to continue the conversation:

πŸ”— Connect with me:

  • LinkedIn - Let's discuss AI development and collaboration opportunities
  • GitHub - Check out more AI projects and contribute to open source

πŸ“š Explore the project:

What will you build with MCP? Drop a comment below with your ideas – I'd love to see what the community creates and help troubleshoot any challenges you encounter!

Follow me here on Dev.to for more tutorials on AI agents, developer tools, and cutting-edge tech. More MCP content coming soon! πŸš€

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.