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
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"]
}
)
]
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)]
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())
Run it with:
python server.py
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"]
}
}
}
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_weatheras 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
cityargument 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
- Add proper authentication and scoped permissions before exposing any server beyond local testing
- Log every tool call, including inputs and outcomes, so failures are debuggable after the fact
- Set reasonable timeouts on any external API calls inside your tools
- Validate all inputs against your declared schema before executing any logic
- 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)