Skip to main content
Glama
cristiangm3005

drillsite-mcp

README.md
# drillsite-mcp

A production-ready **Model Context Protocol (MCP) server** in Python, built on the
official [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk). It lets
an AI agent (Claude Desktop, Claude Code, or any MCP client) query a drill-site
prospectivity API through validated **tools**, **resources** and a **prompt**.

The sample backend is a small FastAPI service that scores mining exploration zones,
modelled on the prospectivity platform I run at [Metalgenomic](https://metalgenomic.cl).
The data here is synthetic.

```
MCP client (Claude, Inspector)
        │  stdio  |  SSE  |  streamable HTTP
        ▼
drillsite-mcp  (MCPServer: tools · resources · prompt)
        │  httpx, API key, timeouts, safe error mapping
        ▼
Drill-site Prospectivity API  (FastAPI + Pydantic)
```

> **FastMCP → MCPServer.** In SDK v2 the `FastMCP` class was renamed `MCPServer`
> (`from mcp.server.mcpserver import MCPServer`). The decorator API is the same.
> To run this on SDK v1, use `from mcp.server.fastmcp import FastMCP` and pin `mcp<2`.

## What it exposes

| Kind | Name | What it does |
|---|---|---|
| Tool | `list_zones(min_score, region)` | Zones at or above a score, best first (read-only) |
| Tool | `get_zone(zone_id)` | One zone with score, drivers and field notes (read-only) |
| Tool | `rank_zones(top_n)` | Top N zones to drill first, with the main driver of each (read-only) |
| Tool | `add_field_note(zone_id, note)` | Writes a field observation to the backend |
| Resource | `zones://all` | All zones as JSON |
| Resource template | `zones://{zone_id}` | One zone as JSON |
| Prompt | `drill_plan(top_n)` | Asks the agent for a drilling order grounded in the tools |

## Production concerns handled

- **Input validation before any request leaves the server.** Zone ids must match
  `Z-000`, scores are bounded 0–1, `top_n` 1–20, notes ≤ 500 characters. Invalid input
  is rejected by the SDK's Pydantic validation and never reaches the API.
- **One shared HTTP client** created in the server lifespan and closed on shutdown,
  with connect/read timeouts and the API key sent as a header.
- **Safe error mapping.** Timeouts, 401/403, 404, 422 and 5xx become short `ToolError`
  messages. Status codes, URLs and keys are logged to stderr, never returned to the model.
- **Prompt-injection mitigation.** Text written by people (field notes) comes back under
  `untrusted_field_notes`, with control characters stripped and length capped. The
  server instructions and the `drill_plan` prompt tell the agent to treat it as data.
- **Tool annotations** (`readOnlyHint`, `destructiveHint`, …) so clients can decide
  which calls need user confirmation.
- **stdout is reserved for the protocol** under stdio; logs go to stderr.
- **End-to-end tests** run MCP client → server → httpx → FastAPI in one process.

## Quick start

Requires Python 3.11+.

```bash
python -m venv .venv
source .venv/bin/activate            # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
pytest                               # 10 tests, all in-process
```

Run the API (terminal 1):

```bash
export DRILLSITE_API_KEY=dev-key     # Windows PowerShell: $env:DRILLSITE_API_KEY="dev-key"
drillsite-api                        # http://127.0.0.1:8000  (docs at /docs)
```

Run the MCP server (terminal 2), on either transport:

```bash
drillsite-mcp --transport stdio                    # for Claude Desktop / Claude Code
drillsite-mcp --transport sse --port 8001          # SSE at http://127.0.0.1:8001/sse
drillsite-mcp --transport streamable-http --port 8001   # http://127.0.0.1:8001/mcp
```

Configuration (environment variables):

| Variable | Default | Purpose |
|---|---|---|
| `DRILLSITE_API_URL` | `http://127.0.0.1:8000` | Backend base URL |
| `DRILLSITE_API_KEY` | `dev-key` | Sent as `X-API-Key` |
| `MCP_TRANSPORT` / `MCP_HOST` / `MCP_PORT` | `stdio` / `127.0.0.1` / `8001` | Defaults for the CLI flags |
| `LOG_LEVEL` | `INFO` | stderr log level |

### Connect from Claude Desktop or Claude Code (stdio)

```json
{
  "mcpServers": {
    "drillsite": {
      "command": "/absolute/path/to/.venv/bin/drillsite-mcp",
      "args": ["--transport", "stdio"],
      "env": { "DRILLSITE_API_KEY": "dev-key" }
    }
  }
}
```

Claude Code: `claude mcp add drillsite -- /absolute/path/to/.venv/bin/drillsite-mcp --transport stdio`

## Debugging with the MCP Inspector

The [MCP Inspector](https://github.com/modelcontextprotocol/inspector) is the official
tool for exercising an MCP server by hand. It needs Node.js. Keep the API running.

**1. UI mode over stdio.** The Inspector launches the server for you:

```bash
npx @modelcontextprotocol/inspector -e DRILLSITE_API_KEY=dev-key drillsite-mcp --transport stdio
```

Open the URL it prints. Click **Connect**, then:
- **Tools → List Tools**: you should see the four tools with their JSON schemas.
- Run `rank_zones` with `top_n = 2` → `Z-001`, `Z-004`.
- Run `get_zone` with `zone_id = ../admin` → a validation error. The request never
  reached the API (check terminal 1: no log line).
- **Resources**: read `zones://all`, then the template `zones://{zone_id}` with `Z-003`.
- **Prompts**: get `drill_plan`.

**2. UI mode over SSE.** Start the server with `--transport sse --port 8001`, then in
the Inspector choose transport **SSE** and URL `http://127.0.0.1:8001/sse`.

**3. CLI mode** (scriptable, good for CI smoke tests):

```bash
npx @modelcontextprotocol/inspector --cli drillsite-mcp --transport stdio --method tools/list
npx @modelcontextprotocol/inspector --cli drillsite-mcp --transport stdio \
  --method tools/call --tool-name rank_zones --tool-arg top_n=2
npx @modelcontextprotocol/inspector --cli drillsite-mcp --transport stdio \
  --method resources/read --uri zones://Z-001
```

**Common problems**

| Symptom | Likely cause |
|---|---|
| Inspector connects, then the server dies at once | Something printed to **stdout**. Under stdio only protocol messages may go there; use logging (stderr). |
| Every tool returns "not authorised" | `DRILLSITE_API_KEY` differs between the API and the MCP server. |
| "The prospectivity API is unreachable" | The API is not running, or `DRILLSITE_API_URL` is wrong. |
| SSE connects but tools time out | Wrong URL: SSE is `/sse`, streamable HTTP is `/mcp`. |

Set `LOG_LEVEL=DEBUG` to see every backend call in stderr.

## Docker

```bash
docker build -t drillsite-mcp .
docker run --rm -p 8001:8001 -e DRILLSITE_API_URL=http://host.docker.internal:8000 drillsite-mcp
```

The image runs the server over SSE on port 8001 as a non-root user.

## Project layout

```
src/drillsite_mcp/api.py      sample FastAPI backend
src/drillsite_mcp/server.py   MCP server: tools, resources, prompt, transports
tests/test_server.py          end-to-end tests through a real MCP client
```

## License

MIT

TDQS

A3.8/5.0

Scored across 4 tools

Disambiguation3/5

list_zones and rank_zones both return zones ordered by score, so an agent may hesitate between threshold-based listing and top-N ranking. The descriptions do distinguish filtering from ranking and driver emphasis, but the overlap is real. get_zone and add_field_note are clearly distinct.

Naming Consistency5/5

All tools follow a consistent snake_case verb_noun pattern: list_zones, get_zone, rank_zones, add_field_note. Singular/plural use is also predictable for one-zone versus many-zone operations.

Tool Count5/5

Four tools is well-scoped for a drill-zone ranking and field-note server. Each tool supports a distinct part of the workflow without excessive surface area.

Completeness4/5

The core workflow of finding, inspecting, ranking, and annotating zones is covered. The main gap is that field notes are append-only, with no update or delete operation, which is a minor but noticeable lifecycle limitation.

Maintenance

ActivityMaintained
ResponsivenessNo issues