DEV Community

Cover image for Build Your First MCP Server: A Practical Guide to Connecting AI Agents to Real Tools
Emma Schmidt
Emma Schmidt

Posted on

Build Your First MCP Server: A Practical Guide to Connecting AI Agents to Real Tools

Learn how to build a working MCP server from scratch and connect it to an AI client, step by step.

Introduction

If you've ever needed an AI agent to check a database, call an internal API, or search a knowledge base, you've probably hit the same wall: every tool integration ends up as custom, one-off code that doesn't transfer to your next project. MCP server development and MCP integration solve this by giving you one standardized way to expose any tool or data source to any compatible AI client, instead of writing bespoke glue code for every combination.

This tutorial walks through building an actual working MCP server, connecting it to a client, and testing that it works end to end. By the end, you'll have a functioning server exposing a real tool, and you'll understand exactly what's happening at each layer instead of just copying a snippet.

Prerequisites

  • Comfortable reading and writing basic Python
  • Python 3.10 or later installed
  • A terminal and a code editor
  • Roughly 30-45 minutes

What We're Building

We're building a small MCP server that exposes one practical tool: looking up the weather for a given city. It's simple enough to fully understand in one sitting, but it demonstrates every core piece of setting up MCP tool integration.

The finished server will be able to:

  • Advertise its available tools to any connecting MCP client
  • Accept a tool call with a specific input (a city name)
  • Return a structured response back to the calling AI agent
  • Run locally so you can test it directly from your terminal

Step 1: Set Up Your Environment

Create a project folder and install the MCP SDK.

mkdir mcp-weather-server
cd mcp-weather-server
python -m venv venv
source venv/bin/activate  # on Windows use venv\Scripts\activate
pip install mcp requests
Enter fullscreen mode Exit fullscreen mode

This gives you the core MCP library plus requests, which we'll use to call a public weather API inside our tool.

Step 2: Define the Server and Register Your Tool

Create a file called server.py. This is illustrative, simplified code meant for learning, not a full production implementation.

# server.py
from mcp.server import Server
from mcp.types import Tool, TextContent

app = Server("weather-server")

@app.list_tools()
async def list_tools():
    return [
        Tool(
            name="get_weather",
            description="Get the current weather for a given city",
            inputSchema={
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "Name of the city"}
                },
                "required": ["city"]
            }
        )
    ]
Enter fullscreen mode Exit fullscreen mode

This list_tools function is how a client discovers what your server can do. When an AI agent connects, it asks "what tools do you have," and this is the answer it gets back, in a structured format it can reason about.

Step 3: Implement What the Tool Actually Does

Now add the logic that runs when the tool is actually called.

# server.py (continued)
import requests

@app.call_tool()
async def call_tool(name: str, arguments: dict):
    if name == "get_weather":
        city = arguments["city"]
        # Simplified example call, replace with a real weather API and key
        response = requests.get(f"https://example-weather-api.test/v1/current?city={city}")
        data = response.json()
        summary = f"The weather in {city} is {data.get('condition', 'unknown')} at {data.get('temp', '?')}°"
        return [TextContent(type="text", text=summary)]
Enter fullscreen mode Exit fullscreen mode

This is the execution layer. The AI model never talks to the weather API directly, it calls your tool through the protocol, and your code decides exactly what happens and what comes back.

Step 4: Run the Server Locally

Add a simple entry point so the server can actually start.

# server.py (continued)
from mcp.server.stdio import stdio_server
import asyncio

async def main():
    async with stdio_server() as (read_stream, write_stream):
        await app.run(read_stream, write_stream, app.create_initialization_options())

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

Run it with:

python server.py
Enter fullscreen mode Exit fullscreen mode

At this point, the server is running and waiting for a client to connect over standard input and output, which is the simplest transport for local development and testing.

Step 5: Connect a Client and Test It

To actually test this, you need an MCP-compatible client configured to launch your server. Most MCP client configurations use a simple JSON entry pointing to your script.

{
  "mcpServers": {
    "weather": {
      "command": "python",
      "args": ["/full/path/to/server.py"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Once your client is configured and connected, ask it something like "what's the weather in Lisbon." If everything is wired correctly, the client will discover your get_weather tool through the list_tools call, invoke it with the city argument, and return the result from your call_tool function back through the model's response.

Testing / Verifying It Works

A few checks to confirm your MCP server integration is actually functioning correctly:

  • Confirm the client's tool list shows get_weather as an available tool after connecting
  • Manually call the tool with a test city and check that the response text looks correct
  • Try an invalid or missing argument and confirm your server doesn't crash silently
  • Restart the client and reconnect to make sure the server initializes cleanly every time, not just on the first run

Common Pitfalls and How to Avoid Them

  • Forgetting to validate input. A missing or malformed city argument will crash a naive implementation. Add basic validation before making the external call.
  • Blocking calls inside async functions. Using a purely synchronous HTTP library inside an async tool handler can freeze your server under load. Consider an async HTTP client for anything beyond a simple learning project.
  • Not handling API failures gracefully. If the weather API is down, your tool should return a clear error message, not throw an unhandled exception that breaks the whole session.
  • Overloading one server with too many unrelated tools. Keeping a server focused on one coherent set of capabilities makes it easier for both you and the connecting AI agent to reason about.

Best Practices for Production Use

  1. Add proper authentication and scoped permissions before exposing any server beyond local testing
  2. Log every tool call, including inputs and outcomes, so failures are debuggable after the fact
  3. Set reasonable timeouts on any external API calls inside your tools
  4. Validate all inputs against your declared schema before executing any logic
  5. Version your tool definitions so you can evolve them without silently breaking existing clients

What to Explore Next

  • Add a second, related tool to the same server, like a multi-day forecast, and see how tool discovery scales
  • Swap the stdio transport for Streamable HTTP to run your server as a proper remote service
  • Explore adding authentication middleware so only verified callers can invoke your tools
  • Look into structured output schemas so responses can be validated programmatically instead of just returned as plain text

Conclusion

Building your first MCP server strips away a lot of the mystery around MCP integration once you've actually wired up discovery, execution, and a real client connection yourself. The pattern you just built scales directly to far more complex tools, databases, internal APIs, search systems, without changing the underlying shape of what you learned here. Once this clicks, adding your next tool is a lot faster than the first one was.

Top comments (0)