an intro to mcp for busy devs
tldr
an mcp server is to an llm what an lsp is to a text editor
mcp is odbc for ai
for the uninitiated, anthropic open sourced model context protocol (mcp) in late november 2024
this is an attempt to create a standardized protocol to make external tools available to llms
by defining a protocol, we avoid having to maintain a "connector" for each llm <-> tool mapping
picking the problem
recently, claude has been playing pokemon on twitch. he might be interested in learning more, so i decided to create a poké api mcp
does he need it?
even though the pokemon gholdengo was released in 2023, wayyyy before claude's knowledge cut off of october 2024, when asked who is pokemon #1000, claude cannot answer it -

giving claude a pokedéx
in their documentation, anthropic uses the uv install script, but if you do that claude can't find uv and run your mcp server.
use `brew install uv` instead
# setup your python project
uv init pokedex
cd pokedex
uv venv
source .venv/bin/activate
uv add "mcp[cli]" httpx
touch poke.py
from typing import any
import httpx
from mcp.server.fastmcp import fastmcp
mcp = fastmcp("poke")
poke_api = "https://pokeapi.co/api/v2/"
sets up the basics: imports tools for typing and http requests, grabs fastmcp, and defines the pokeapi base url
async def make_pokeapi_request(url: str) -> dict[str, any]|none:
async with httpx.asyncclient() as client:
try:
response = await client.get(url, timeout=30.0)
response.raise_for_status()
return response.json()
except httpx.httperror as e:
print(f"http error occurred '{url}': {e}")
return none
except exception as e:
print(f"an unexpected error occurred '{url}': {e}")
return none
a helper function that async-fetches data from a url using httpx, spits out json if it works
@mcp.tool()
async def get_pokemon_details(pokemon_name_or_id: any) -> dict[str, any]:
"""get detailed information about a pokémon"""
url = f"{poke_api}pokemon/{pokemon_name_or_id}"
pokemon_data = await make_pokeapi_request(url)
if not pokemon_data:
return {"error": f"couldn't get details - '{pokemon_name_or_id}'"}
return {
"id": pokemon_data["id"],
"name": pokemon_data["name"].capitalize(),
"types": [t["type"]["name"].capitalize() for t in pokemon_data["types"]],
"abilities": [
a["ability"]["name"].replace("-", " ").capitalize()
for a in pokemon_data["abilities"]
],
"base_experience": pokemon_data["base_experience"],
"height": pokemon_data["height"],
"weight": pokemon_data["weight"],
}
grabs pokémon info by name or id from pokeapi, formats it nice (capitalized names, clean ability list), or returns an error if it fails
@mcp.tool()
async def get_ability_details(ability_name_or_id: any) -> str:
"""get detailed information about a pokémon ability."""
url = f"{poke_api}ability/{ability_name_or_id}"
ability_data = await make_pokeapi_request(url)
if not ability_data:
return f"could not retrieve detailed information"
return ability_data
fetches ability details by name or id from pokeapi, returns the raw data or an error message if it can’t
if __name__ == "__main__":
mcp.run(transport='stdio')
don't forget to run the server
@mcp.tool() turns your functions into server-ready endpoints that can be triggered by claude
so we have exposed 2 functions to claude -
get_ability_details and get_pokemon_details
{
"mcpservers": {
"pokedex": {
"command": "uv",
"args": [
"--directory",
"/absolute/path/to/parent/folder/pokedex",
"run",
"poke.py"
]
}
}
}
you will need to restart your claude app for this to work, also this is not available on the claude.ai yet, but it is in dev according to anthropic
results
we gave claude a pokedex, now let's see him use it -

anthropic also suggests using llms themselves to build mcp servers
however, that exercise is left up to the reader :)