Tools

Step 1. Create a directory called tools.

mkdir tools

Step 2. Change your working directory to the directory you just created.

cd tools

Step 3. Create a file called requirements.txt and add the content below to it.

fastmcp

Step 4. In the tools directory, create a directory called squidfall.

mkdir squidfall

Step 5. Inside the squidfall directory you just created, create a file called __init__.py

Step 6. Inside the squidfall directory, create a file called main.py and add the content below to it.

# Standard library imports.
from json import dumps
from logging import getLogger
from os import environ, getenv

# Third party imports.
from fastmcp import FastMCP
from fastmcp.utilities.logging import configure_logging
from httpx import AsyncClient
from starlette.requests import Request
from starlette.responses import PlainTextResponse

# Init a MCP server and set its logging level.
mcp = FastMCP(name="squidfall")
configure_logging(level="DEBUG")
logger = getLogger("squidfall")

GEOCODING_API_KEY = environ["GEOCODING_API_KEY"]


@mcp.custom_route("/api/v1/healthcheck", methods=["GET"])
async def health_check(request: Request) -> PlainTextResponse:
    return PlainTextResponse(dumps({"status": "ok"}))


@mcp.tool(description="Get the latitude and longitude for a location.")
async def get_coordinates(location: str) -> dict:
    """Get the latitude and longitude for a location.

    Args:
        location: A city name, address, or ZIP code (e.g. 'Pittsburgh, PA').

    Returns:
        A dict with 'lat' and 'lon' keys, or an error message under 'error'.
    """
    async with AsyncClient(verify=False) as client:
        response = await client.get(
            "https://geocode.maps.co/search",
            params={
                "q": location,
                "api_key": GEOCODING_API_KEY,
            },
            follow_redirects=True,
        )
        response.raise_for_status()
        results = response.json()
        if not results:
            return {"error": f"No coordinates found for: {location}"}

        return {"lat": float(results[0]["lat"]), "lon": float(results[0]["lon"])}


@mcp.tool(description="Get the current weather forecast for a coordinate pair.")
async def get_forecast(lat: float, lon: float) -> str:
    """Get the current weather forecast for a coordinate pair.

    Args:
        lat: Latitude.
        lon: Longitude.

    Returns:
        The current forecast period as a plain text string.
    """
    async with AsyncClient(verify=False) as client:
        points_response = await client.get(
            url=f"https://api.weather.gov/points/{lat},{lon}",
            headers={
                "User-Agent": "(squidfall, contact@example.com)",
                "Accept": "application/geo+json",
            },
            follow_redirects=True,
        )
        points_response.raise_for_status()
        forecast_url = points_response.json()["properties"]["forecast"]
        forecast_response = await client.get(forecast_url)
        forecast_response.raise_for_status()
        periods = forecast_response.json()["properties"]["periods"]
        current = periods[0]

        return f"{current['name']}: {current['detailedForecast']}"


if __name__ == "__main__":
    mcp.run(
        transport="streamable-http",
        host="0.0.0.0",
        port=8002,
    )

Step 7. In the tools directory, create a file called .env and add the content below to it.

export GEOCODING_API_KEY=""

Step 8. In the tools directory, create a file called Dockerfile and add the content below it. Feel free to modify the image.authors label.

FROM alpine:3.23
LABEL image.authors="Victor Fernandez III, @cyberphor"
WORKDIR /home/tools/
COPY requirements.txt requirements.txt
COPY squidfall/ squidfall/
RUN apk add --no-cache --update python3 py3-pip &&\
    pip install --break-system-packages -r requirements.txt &&\
    adduser -D squidfall -h /home/tools/ &&\
    chown -R squidfall:squidfall /home/tools/
USER squidfall
EXPOSE 8002
CMD [ "python", "-m", "squidfall.main" ]

Step 9. From the root of the repository, run the command below to start the tools container.

make DOCKER_COMPOSE_PROFILE=tools

Step 10. Run the command below to start the container you just created.

make DOCKER_COMPOSE_PROFILE=tools start

Step 11. Run the command below to confirm the container has started.

make DOCKER_COMPOSE_PROFILE=tools status

Step 12. If the container has started, run the commands below to interact with it.

curl http://localhost:8002/api/v1/healthcheck && echo

You should get output similar to below.

{"status": "ok"}

Step 13. Run the command below to stop the container.

make DOCKER_COMPOSE_PROFILE=tools stop