AI & Agents September 16, 2026 12 min read

Building our first MCP and using it

A white USB-C cable on a dark background — the universal port MCP wants to be for AI. Photo by Mishaal Zahed on Unsplash.

Desktop tools like Anthropic's CLI tool, Claude Code, Opencode, etc. have completely changed the way we develop software directly from the terminal. But no matter how clever we are at reading and editing code, artificial intelligence often remains isolated from your company's real ecosystem—closed off without direct access to internal databases, proprietary APIs, or workflow-specific local tools. This is where the Model Context Protocol (MCP) comes in.

Think of MCP as a universal USB-C port for Artificial Intelligence. Created as an open standard, it allows us to create a secure bridge so that these tools can interact with any external tool or data source in real time.

In this short set of words, we'll go from theory to action.

We will learn how to:

  • Design and program our first custom MCP server using Python.
  • Securely set up the local environment to expose the functions we need.
  • Inspect and test MCP server functionalities
  • Connect the server directly to our tools to automate complex tasks without leaving the command line.
  • Connect AI client apps to the MCP server and use it.

First thoughts

The objective is to create an MCP server that provides some tools, resources and prompts related to world cities and countries information. We will have the following features:

NameTypeDescription
get_city_detailstoolGets the details of a city from a country code and the city name.
get_cities_in_countrytoolGets a list of the cities of a country
all_countriesResource (URI)Gets the list of all country codes
country_nameResource (template)Gets the country name from country code
city_details_verbosepromptGathers detailed information about a city and returns it formatted in an HTML table.

Then, to create an MCP client to use these functionalities.

Finally, to configure the MCP server on a project so that a desktop tool like Claude Code can use it.

Hands-on

Step 1: First, create a virtual environment and add the dependencies. Run this commands on the folder you want to use.

Shell
python3 -m venv .venv
source .venv/bin/activate
pip install "mcp<2" pydantic "mcp[cli]"

This will create a virtual environment and will install 2 python packages needed for the mcp ecosystem:

  • mcp: for mcp functionalities. At this time, we will limit it to a version lesser than 2
  • pydantic: for data validation and settings management based on Python type hints.
  • mcp[cli]: additional console tools to work with mcp

Note: I am using pip to install the packages, but we could use uv instead.

Shell
uv venv .venv
source .venv/bin/activate
uv pip install "mcp<2" pydantic "mcp[cli]"

Step 2: Next, lets create the following files:

File nameDescription
mcp_server.pyMCP server implementation
mcp_client.pyMCP client implementation to run in shell
operations.pyMCP server background functions
README.mdProject “visit card” and project instructions

Run the following commands.

Shell
touch mcp_server.py
touch mcp_client.py
touch operations.py
touch README.md

Step 3: Another file we will need is a dataset with world cities information. Lets get it from Kaggle:

World Cities Database: https://www.kaggle.com/datasets/max-mind/world-cities-database

Kaggle page for the World Cities Database dataset by MaxMind

The CSV file has the following format:

cities.csv open in an editor: columns Country, City, AccentCity, Region, Population, Latitude and Longitude

Rename it to cities.csv and add it to our project folder.

Step 4: Add the code to load the data from csv file to the operations.py file.

Python
import csv
import unicodedata
from pathlib import Path

_CITIES_PATH = Path(__file__).with_name("cities.csv")
try:
    with open(_CITIES_PATH, "r") as f:
        _cities = list(csv.DictReader(f))
except Exception as e:
    print(e)
    _cities = []

def _normalize_city_name(city_name: str) -> str:
    normalized_text = unicodedata.normalize('NFD', city_name)
    no_accent_text = "".join(
        c for c in normalized_text if unicodedata.category(c) != 'Mn'
    )
    return no_accent_text

This will expose the dataset as operations.cities and also a function to normalize city name.

Step 5: Create MCP server. Open mcp_server.py and add the following code run the MCP server.

Python
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("cities mcp")

if __name__ == "__main__":
    print("Running MCP server...")
    mcp.run()

Note: At this time the server code is runnable but we don’t have any mcp tool nor a resource or a prompt added to the server and ready to be used.

Shell
python mcp_server.py
Terminal running python mcp_server.py and printing 'Running MCP server...'

Now it is time to talk about MCP Inspector. This is an interactive developer tool for testing and debugging MCP servers, in the browser, on the command line, and in the terminal.

More info here: https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector

Even with no MCP tools, resources or prompts we can run MCP Inspector on our MCP server. Run the following command:

Shell
mcp dev mcp_server.py

This will start the MCP Inspector.

Terminal output of mcp dev mcp_server.py: MCP Inspector starting and opening the browser

And will open a browser window with the MCP Inspector and our mcp_server ready to be connected to.

MCP Inspector landing page with the server listed as Disconnected

Lets connect to the server.

MCP Inspector connected to 'cities mcp', with the protocol messages for tools, prompts and resources on the right

On the right side, we will see that some messages were already exchanged to get the list of resources, prompts and tools ( they are empty so far, as we know ).

Step 6: Create and add the get_city_details tool to the MCP server. Go to mcp_server.py and add the import, the region and the mcp tool.

Python
from mcp.server.fastmcp import FastMCP
from operations import get_city_details

mcp = FastMCP("cities mcp")

#region TOOLS
mcp.tool(
    name="get_city_details",
    description="Gets the detais of a city from a country code and the city name.",
)(get_city_details)

#endregion TOOLS

if __name__ == "__main__":
    print("Running MCP server...")
    mcp.run()

Step 7: get_city_details tool is pointing to the operations.get_city_details function, which is not created yet. Lets create it. This is the code.

Python
def get_city_details(
    country_code: str = Field(description="The country code in 2 letter"),
    city_name: str = Field(description="The city name"),):
    norm_city_name = _normalize_city_name(city_name=city_name)
    found_record = next(
        (
            item for item in _cities if item.get("City", "").lower() == norm_city_name.lower()
            and item.get("Country", "").lower() == country_code.lower()
        ),
        None
    )
    return found_record

This function receives 2 parameters: country_code and city_name. These parameters are defined as pydantic Field instances. We will need to add the following import for this definition.

Python
from pydantic import Field

Step 8: Go back to the MCP Inspector and open the Tools separator. Our tool will be there.

MCP Inspector Tools tab showing the get_city_details tool

Select the tool name to run a test.

MCP Inspector form for get_city_details with country code 'pt' and city name 'Amadora'

I tested with my hometown (“Amadora”) and country code (“pt”). Here are the results:

MCP Inspector results: JSON with the details of Amadora, Portugal

This is our first MCP server call.

Step 9: Create the get_cities_in_country MCP tool

Add the following function to operations.py.

Python
def get_cities_in_country(
    country_code: str = Field(description="The country code in 2 letter"),):
    found_records = [item["AccentCity"] for item in _cities if item["Country"] == country_code]
    return found_records

Add the following tool to mcp_server.py.

Python
mcp.tool(
    name="get_cities_in_country",
    description="Gets a list of the cities of a country.",
)(get_cities_in_country)

Also import the operations.get_cities_in_country function.

Python
from operations import get_city_details, get_cities_in_country

Test it on the MCP Inspector.

MCP Inspector form for get_cities_in_country with country code 'pt'

Check the results.

MCP Inspector results listing Portuguese cities: Abação, Abaças, Abade de Neiva and more

Pause moment: Tools vs resources vs prompts

Building with Model Context Protocol (MCP) or designing our AI application requires choosing the right primitive to maximize efficiency. Navigating these choices can be simplified with a quick decision guide based on our core objectives:

Use tools when we need to give our client app new capabilities, allowing it to execute actions, run calculations, or interact dynamically with external systems.

Here is use-case: A calculate_mortgage_payment tool.

When a user asks, "What would my monthly payment be for a €400k house?", client app calls this tool with parameters like interest rate and loan term. The tool runs the math and returns the exact calculation back to the client app.

Use resources when you need to get data into your app for UI or context, providing the model with background information, files, or live database snapshots.

Here is a use-case: A git_repository_file read-only data stream.

Our app feeds the contents of a project's README.md or a database schema directly into client app’ context window. Client app reads this data to understand the code structure before answering developer questions.

Use prompts when you want to create predefined workflows for users, establishing structured guidelines, templates, or predictable behaviors that standardize the user experience.

Here is a use-case: A code_reviewer template.

A predefined slash command (like /review) that automatically injects a structured system prompt into the chat: "You are an expert staff engineer. Review the following code snippet for security vulnerabilities and performance bottlenecks."

Hands-on part 2

Step 10: Jump to the creation of the all_countries URI resource.

First step is to get a csv file with the list of country codes and names. I got it from github datasets: https://github.com/datasets/country-list/blob/main/data.csv

The CSV file has the following format:

countries.csv open in an editor: columns Name and Code

Rename it to countries.csv and add it to our project folder.

Project folder with .venv, cities.csv, countries.csv, mcp_client.py, mcp_server.py, operations.py and README.md

Then, add the following function to operations.py file.

Python
def get_countries():
    _COUNTRIES_PATH = Path(__file__).with_name("countries.csv")
    try:
        with open(_COUNTRIES_PATH, "r") as f:
            _countries = f.read()
            return _countries
    except Exception as e:
        print(e)
        return ""

Lets continue, by adding the following code to mcp_server.py.

Python
#region RESOURCES
mcp.resource(
    "cities://countries/",
    mime_type="text/csv"
)(get_countries)
#endregion RESOURCES

And update the imports

Python
from operations import get_city_details, get_cities_in_country, get_countries

This will add a URI resource accessible by the URI: cities://countries/

Step 11: Next, we will add the country_name template resource. Add the following function to operations.py file.

Python
def country_name(
    country_code: str = Field(description="The country code in 2 letter"),):
    _COUNTRIES_PATH = Path(__file__).with_name("countries.csv")
    try:
        with open(_COUNTRIES_PATH, "r") as f:
            _countries = list(csv.DictReader(f))
            found_record = next(
                (
                    item for item in _countries if item.get("Code", "").lower() == country_code.lower()
                ),
                    None
            )
            return found_record["Name"]
    except Exception as e:
        print(e)
        return ""

Add this function to the imports on mcp_server.py.

Python
from operations import get_city_details, get_cities_in_country, get_countries, country_name

Add the following code to mcp_server.py in order to add the resource template.

Python
mcp.resource(
    "cities://country_name/{country_code}",
    mime_type="text/plain"
)(country_name)

This resource template will be accessible by the URI:

cities://country_name/{country_code}

And will be expecting a the country code as a parameter on the {country_code} placeholder.

Step 12: At this time, we can see the 2 resources on the MCP Inspector.

MCP Inspector Resources tab showing one URI (get_countries) and one template (country_name)

We should test them.

get_countries

MCP Inspector rendering the cities://countries/ resource as a table of country names and codes

country_name

MCP Inspector form for the country_name template with country_code 'pt'
MCP Inspector result for cities://country_name/pt: Portugal

Step 13: Going on the same flow, lets create now the city_details_verbose MCP prompt.

Add the following function to the operations.py file.

Python
def get_city_details_prompt(
    city: str = Field(description="The name of the city"),
    country_code: str = Field(description="The 2-letter ISO country code (e.g., pt, us, fr)")) -> str:
    prompt = f"""
    Your goal is to provide comprehensive details about this city: {city} on a country with this Country Code: {country_code}

    Use the 'get_city_details' tool and compile key information about this city:
    - Country
    - City
    - AccentCity
    - Region
    - Population
    - Latitude
    - Longitude

    Present all of this information strictly in a clean, well-structured HTML table (`<table>`). Ensure the table is easy to read, uses proper table headers (`<th>`), and does not include any markdown styling around the HTML code.
"""

    return prompt

Add this function to the imports on mcp_server.py.

Python
from operations import (
    get_city_details,
    get_cities_in_country,
    get_countries,
    country_name,
    get_city_details_prompt,
)

Add the following code to mcp_server.py in order to add the prompt.

Python
#region MCP Prompts
mcp.prompt(
    name="city_details",
    description="Gathers detailed information about a city and returns it formatted in an HTML table.",
)(get_city_details_prompt)
#endregion MCP Prompts

Now, test it on the MCP Inspector.

MCP Inspector Prompts tab: city_details prompt with city 'Sintra' and country_code 'pt'

Here is the generated prompt.

MCP Inspector showing the user message generated by the city_details prompt

Second pause: A review on what we have achieved

At this time we have:

  • An MCP server running locally
  • 2 MCP server tools
  • 2 MCP resources ( URI and template )
  • 1 MCP prompt

Now we can create a python MCP client script that interacts with the MCP server.

We need to have in mind that an MCP client interaction with an MCP server needs 3 things:

  1. The Connection (Transport): This is the physical or virtual basis of communication. Before any data exchange, the client needs to establish a stable transport route with the server (either locally via stdio or remotely via HTTP/SSE).
  2. The Session (Context): Once the connection is established, an active session is created. This is the stage where the initial handshake occurs, where the client and server negotiate capabilities, discover available tools, and establish the lifecycle of that specific interaction.
  3. The Command (Action): With the road built (Connection) and the rules defined (Session), the AI ​​can finally act. The client sends specific commands—such as requesting data from a file or executing a customized tool—and the server processes them, returning the response.

Hands-on part 3

Step 14: Add the following code to the mcp_client.py file.

Python
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

params = StdioServerParameters(command="python", args=["mcp_server.py"])

async def main() -> None:
    # connection
    async with stdio_client(params) as (read, write):
        # session
        async with ClientSession(read, write) as session:
            await session.initialize()

            # command
            country_code = "pt"
            city_name = "Faro"
            result = await session.call_tool(
                "get_city_details",
                {"country_code": country_code, "city_name": city_name, }
            )
            result = result.content[0].text
            print(result)

asyncio.run(main())

This client will call the get_city_details tool with country_code = “pt” and city_name = “Faro”. For that we are using the session.call_tool() function.

Python
            # command
            country_code = "pt"
            city_name = "Faro"
            result = await session.call_tool(
                "get_city_details",
                {"country_code": country_code, "city_name": city_name, }
            )

Lets run the code.

Shell
python mcp_client.py
Terminal running python mcp_client.py and printing the JSON details for Faro

We can do the same for:

resources → session.read_resource()

Python
            # command
            result = await session.read_resource(
                "cities://countries/",
            )
            result = result.contents[0].text

Running the code.

Terminal running python mcp_client.py and printing the countries CSV returned by the resource

prompts → session.get_prompt()

Python
            # command
            country_code = "pt"
            city_name = "Faro"
            result = await session.get_prompt(
                "city_details",
                {"city": city_name, "country_code": country_code, },
            )
            result = result.messages
            for message in result:
                print(message.content.text)

Running the code.

Terminal running python mcp_client.py and printing the generated prompt text

Step 15: Last, but not least, lets use this MCP on an AI client app, like Claude Code.

To do so, head to the MCP Inspector landing page and press the Export button, on the top right side.

MCP Inspector landing page with the Export button in the top right corner

A file named mcp.json will be downloaded. Copy the file to the project folder and rename it to .mcp.json (just add the point on the beginning of the file name). Then open the file. It should look like this.

JSON
{
  "mcpServers": {
    "my_mcp_server": {
      "type": "stdio",
      "command": "uv",
      "args": [
        "run",
        "--with",
        "mcp",
        "mcp",
        "run",
        "mcp_server.py"
      ],
      "cwd": "/Users/sergioildefonso/my_mcp"
    }
  }
}

This file instructs Claude Code how to run the MCP.

Change the “uv” on the third line for a name you want to give to your MCP server ( example: my_mcp_server ).

Next, open Claude Code on the project folder. We will see a message from Claude Code stating that a new MCP server was found.

Claude Code prompt: 'New MCP server found in this project: my_mcp_server' with the option 'Use this MCP server'

Select the “Use this MCP server” option.

Lets write /mcp on the Claude Code command line and see the the Project MCP we created.

Claude Code /mcp screen listing the project MCP my_mcp_server as connected with 2 tools

Lets test it.

Ask the following to Claude Code:

“get me the details of the city Amadora in country pt”

Here are the results.

Claude Code answering 'get me the details of the city Amadora in country pt' after calling my_mcp_server

We can see that Claude Code called our MCP.

A bonus

The simple MCP server implementation we did was done with a local connection (stdio). If we want to implement a remote connection (HTTP/SSE), we should do the following changes.

Step 16: Create a new file called mcp_server_http.py. On it add the following code.

Python
from mcp.server.fastmcp import FastMCP
from operations import (
    get_city_details,
    get_cities_in_country,
    get_countries,
    country_name,
    get_city_details_prompt,
)

mcp = FastMCP("cities mcp", host="127.0.0.1", port=8000)

# region TOOLS
mcp.tool(
    name="get_city_details",
    description="Gets the detais of a city from a country code and the city name.",
)(get_city_details)

mcp.tool(
    name="get_cities_in_country",
    description="Gets a list of the cities of a country.",
)(get_cities_in_country)
# endregion TOOLS

# region RESOURCES
mcp.resource("cities://countries/", mime_type="text/csv")(get_countries)

mcp.resource("cities://country_name/{country_code}", mime_type="text/plain")(
    country_name
)

# endregion RESOURCES

#region MCP Prompts
mcp.prompt(
    name="city_details",
    description="Gathers detailed information about a city and returns it formatted in an HTML table.",
)(get_city_details_prompt)
#endregion MCP Prompts

if __name__ == "__main__":
    print("Running MCP server on http://127.0.0.1:8000/sse ...")
    mcp.run(transport="sse")

Step 17: Create a new file called mcp_client_http.py. On it add the following code.

Python
import asyncio
from mcp import ClientSession
from mcp.client.sse import sse_client

SERVER_URL = "http://127.0.0.1:8000/sse"

async def main() -> None:
    # connection (server must already be running: python mcp_server_http.py)
    async with sse_client(SERVER_URL) as (read, write):
        # session
        async with ClientSession(read, write) as session:
            await session.initialize()

            # command
            country_code = "pt"
            city_name = "Faro"
            result = await session.call_tool(
                "get_city_details",
                {"country_code": country_code, "city_name": city_name, }
            )
            result = result.content[0].text
            print(result)

            # command
            result = await session.read_resource(
                "cities://countries/",
            )
            result = result.contents[0].text
            print(result)

            # command
            country_code = "pt"
            city_name = "Faro"
            result = await session.get_prompt(
                "city_details",
                {"city": city_name, "country_code": country_code, },
            )
            result = result.messages
            for message in result:
                print(message.content.text)

asyncio.run(main())

As we can see, these files are clones of the local server and client files but with different connection properties:

  • On the server, we define the connection url + port and configures the server to communicate over a network using Server-Sent Events (SSE) for real-time, server-to-client streaming.
  • On the client, we import the sse_client pointing to the server url + port

In this case, the content of the .mcp.json file should be:

JSON
{
  "mcpServers": {
    "my_mcp_http_server": {
      "type": "sse",
      "url": "http://127.0.0.1:8000/sse"
    }
  }
}

That’s all

At this time we are able to create a local MCP server with tools, resources and prompts. Also we can test it and connect apps to it ( like Claude Code ) or build custom client scripts.

I created the following GitHub repository with the code and the files I used to create this project. It can be accessed here: https://github.com/sIldefonsoRR/my_mcp

GitHub repository sIldefonsoRR/my_mcp with the project files