an intro to mcp for busy devs






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 -

without pokeapi mcp
image 1


giving claude a pokedéx


    
# 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

add this to `~/library/application\ support/claude/claude_desktop_config.json`
    
{
    "mcpservers": {
        "pokedex": {
            "command": "uv",
            "args": [
                "--directory",
                "/absolute/path/to/parent/folder/pokedex",
                "run",
                "poke.py"
            ]
        }
    }
}
    
        


results


we gave claude a pokedex, now let's see him use it -
image 2
claude uses the first function to find pokemon #1000, answers question #1 and #2, and then uses the information from that response to fire another api call to answer question #3

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