Skip to main content
Glama
jasonwarta

swapi_mcp

by jasonwarta
README.md
# swapi_mcp

A small end-to-end reference project for building an **observable AI agent**: an
[MCP](https://modelcontextprotocol.io) server wrapping the
[Star Wars API](https://swapi.info/api), a Claude agent that drives it, and a web
chat UI — all instrumented with **OpenTelemetry** and **Grafana Cloud** (distributed
traces, RED metrics, frontend RUM, and AI/agent observability).

It exists to show, in one runnable codebase, how the pieces fit together — the
MCP tool boundary, the agent's tool-use loop, and full-stack tracing from a
browser click all the way down to an upstream HTTP call and back.

## What's in here

Two halves that talk to each other over MCP (stdio):

**The MCP server** (`src/index.ts`, `src/tools/`) — wraps the Star Wars API as
18 tools across 6 resources (people, planets, films, species, vehicles,
starships): a `search_`, a get-all, and a get-by-id per resource. It's a plain
tool provider; it makes no decisions.

**The agent** (`src/agent/`) — the decision-maker:

- `mcpClient.ts` — spawns the built server over stdio and connects as an MCP client.
- `agent.ts` — a manual tool-use loop: send the question + the server's tools to
  Claude, execute each requested tool via MCP, feed results back, repeat until done.
- `main.ts` — a CLI entry point.
- `server.ts` + `public/index.html` — an HTTP server and a single-page chat UI.

## Observability

Everything below is **optional and gated on env vars** — the agent runs fine
without any of it.

- **Distributed tracing** (OpenTelemetry → Tempo): one trace spans
  `browser → POST /ask → agent.turn → llm.request / tool.call → tool.execute → swapi.fetch`,
  across three services (`swapi-web`, `swapi-agent`, `swapi-mcp-server`). Trace
  context is propagated across the MCP boundary via the request `_meta`.
- **RED metrics** (→ Mimir): rate / errors / duration histograms per tool and per
  LLM call, plus a token-usage counter for cost, tagged with `model`, `error`, and
  `error.type`.
- **Frontend RUM** ([Grafana Faro](https://grafana.com/oss/faro/)): Web Vitals, JS
  errors, and browser→backend trace propagation.
- **AI / agent observability** ([`@grafana/agent-o11y`](https://www.npmjs.com/package/@grafana/agento11y)):
  normalized LLM generations and tool executions.
- **Demo fault injection** (`src/chaos.ts`): optionally makes one resource return a
  simulated 503 and another run 50% slower, so the dashboards show real error and
  latency signals.

## Prerequisites

- Node.js 22+ (uses `--env-file`)
- [Yarn](https://yarnpkg.com/) 4 (via Corepack)
- An Anthropic API key (for the agent)
- A Grafana Cloud account (optional — only for the observability features)

## Setup

```bash
yarn install
yarn build
```

### Use the MCP server directly (e.g. Claude Desktop)

Point your MCP client at the built server:

```json
{
  "mcpServers": {
    "swapi": { "command": "node", "args": ["/absolute/path/to/swapi_mcp/build/index.js"] }
  }
}
```

### Run the agent

Create a `.env` (git-ignored) with at least your Anthropic key:

```bash
ANTHROPIC_API_KEY=sk-ant-...
```

Then:

```bash
yarn dev                              # build + serve the web UI at http://localhost:8787
yarn ask "Who is Luke's homeworld?"   # one-shot CLI
```

## Environment variables

| Variable | Purpose |
|---|---|
| `ANTHROPIC_API_KEY` | Required for the agent. |
| `ANTHROPIC_MODEL` | Model override (default `claude-sonnet-5`). |
| `PORT` | Web server port (default `8787`). |
| `OTEL_EXPORTER_OTLP_ENDPOINT` / `_HEADERS` / `_PROTOCOL`, `OTEL_SERVICE_NAME` | Enable OTLP traces + metrics to Grafana Cloud. |
| `FARO_URL`, `FARO_APP_NAME` | Enable Grafana Faro frontend RUM (the collector URL is public by design). |
| `AGENTO11Y_ENDPOINT`, `AGENTO11Y_AUTH_TENANT_ID`, `AGENTO11Y_AUTH_TOKEN`, `AGENTO11Y_PROTOCOL`, `AGENTO11Y_AUTH_MODE` | Enable Grafana Agent Observability (the token needs the `sigil:write` scope). |
| `CHAOS_ENABLED`, `CHAOS_FLAKY_RESOURCE`, `CHAOS_SLOW_RESOURCE` | Demo fault injection (off by default). |

## Notes

This is a learning / demo project. The data comes from a static mirror of SWAPI,
which models *associations* (a character "appears in" a film), not actions — so
answers are phrased accordingly.

## License

[MIT](./LICENSE)