Skip to main content
Glama
yaralahruthik

Nightjar MCP Server

README.md
# Build an MCP Server

Companion repo for [Build an MCP Server](https://frontendhire.com/learn/ai/courses/build-an-mcp-server/overview) on Frontend Hire.

You wrap Nightjar, a privacy-first analytics product, in an MCP server that Claude
Code can use. The API is given. Your server is the whole assignment.

## Start here

Every lesson ends at a tag. Branch from the starter so you are never in detached
HEAD:

```bash
git clone https://github.com/yaralahruthik/build-an-mcp-server
cd build-an-mcp-server
git checkout -b my-build 00-starter
bun install
cp .env.example .env
bun run seed
```

Then, in one terminal:

```bash
bun run api
```

Nightjar is now on `http://localhost:8787`. Leave it running. Everything you write
lives in `src/` and `scripts/`, in a second terminal.

## What you need

Bun, an editor, and Claude Code. **No API key.** Nothing in this repo calls a
model, so your Claude subscription is the entire cost of the course. Lesson 11
adds a Cloudflare account, and the free tier covers it.

## What is in the starter

- `api/` is Nightjar: a Hono server over a seeded SQLite database. **Do not edit
  it.** Six lessons exist because this API is awkward in ways you cannot fix at the
  source, which is the situation you are actually in at work.
- `src/server.ts` is empty. Lesson one fills it.

The seed is deterministic: 121,541 events over the 90 complete days before today,
120,000 users, three funnels, four dashboards. Every count the lessons print is a
count you will see, because the data is anchored to *today* rather than to a fixed
date, and every window in the course is a whole number of complete days.

Today itself is empty on purpose. "Yesterday" is the most recent window with data
in it.

## Nightjar's API

| Endpoint | Notes |
| --- | --- |
| `GET /v1/events` | Cursor paginated, 100 rows per page maximum, no total count |
| `GET /v1/events/count` | Totals, optionally grouped by a property |
| `GET /v1/funnels` | Funnel ids and their step names |
| `GET /v1/funnels/:id` | Distinct users per step. Counts only, never rates |
| `GET /v1/metrics/timeseries` | Slow (about 3.5 seconds), and rate limited to 3 calls a minute |
| `GET /v1/dashboards` | Dashboard metadata |
| `GET /v1/dashboards/:id` | One dashboard |

All of them want RFC 3339 timestamps, take `Authorization: Bearer <key>`, and
return errors as `{"error":{"code","message"}}`.

## Deploying it (lesson 11)

The MCP server runs on Cloudflare Workers with no code changes: `src/worker.ts`
hands the same `Request` to the same handler.

```bash
cp .dev.vars.example .dev.vars   # local secrets for `bun run worker:dev`
bun run worker:dev               # the Worker, under workerd, on :8790

wrangler secret put MCP_AUTH_TOKEN
wrangler secret put NIGHTJAR_API_KEY
wrangler secret put NIGHTJAR_API_URL
bun run deploy
```

`wrangler dev` reaches the Nightjar on your laptop. The deployed Worker cannot,
so until `NIGHTJAR_API_URL` points somewhere public, `tools/list` will work and
every tool call will fail. The lesson covers both honest ways to finish the
loop.

## Pointing it at your own API (lesson 12)

Everything Nightjar-specific lives in `src/source/nightjar.ts`. The tools talk
to the `AnalyticsSource` interface in `src/source/types.ts`, which asks four
questions and gets back data or a classified failure.

To swap the product:

1. Implement `AnalyticsSource` for your API in `src/source/your-api.ts`.
2. Change one line in `src/source/index.ts`.
3. Change the enums in `src/tools/count-events.ts` to your event names and your
   properties, and mark the user-supplied ones in `src/untrusted.ts`.

Nothing else moves. The results, the error sentences, the fencing, the transport,
and the auth all keep working, because none of them ever knew whose API it was.

## Checkpoints

| Tag | Lesson |
| --- | --- |
| `00-starter` | Overview |
| `01-the-smallest-server-that-connects` | 1 |
| `02-where-the-naive-tool-breaks` | 2 |
| `03-constraining-the-schema` | 3 |
| `04-writing-the-result-for-a-reader` | 4 |
| `05-errors-that-recover` | 5 |
| `06-before-the-cut` | 6, the seven-tool server before the deletion |
| `06-how-many-tools-is-too-many` | 6 |
| `07-resources-and-prompts` | 7 |
| `08-your-own-users-write-your-prompt` | 8 |
| `09-from-stdio-to-http` | 9 |
| `10-who-is-calling` | 10 |
| `11-deploying-it` | 11 |
| `12-point-it-at-your-own-api` | 12 |

Join at any point with `git checkout -b my-build <tag>`, or diff your work against
the reference with `git diff <previous-tag> <tag>`.

## If something looks wrong

**"Yesterday" returns nothing.** The database has aged past its window. Run
`bun run seed` again.

**Every request returns `unauthorized`.** Your `.env` is missing, or
`NIGHTJAR_API_KEY` does not match what the server started with. Restart
`bun run api` after editing `.env`.

**The timeseries endpoint returns 429.** That is the endpoint doing its job. Wait
for the `Retry-After` window, which is at most a minute.

Maintenance

ActivityMaintained
ResponsivenessNo issues