# MCP Server Development

> **[Docs Overview](./overview.md)** / MCP Server Development

How the `dealer-lookup` MCP server is built and how to extend it. This is the **developer** companion to the operational docs:

- [MCP_SERVER.md](./MCP_SERVER.md) - deployment, PM2, security (IP allowlist, bearer token, rate limit), token rotation.
- [MCP_CONFIGURATION.md](./MCP_CONFIGURATION.md) - the repo-level `.mcp.json` and Claude Code client setup.

---

## What it is

A standalone, **read-only** MCP server that gives admins (via Claude Code) live lookups against the production database and Stripe, plus access to this `docs/` tree. It runs as its own PM2 process over an HTTP (StreamableHTTP) transport - it is _not_ part of the Next.js app. Source lives under `mcp/dealer-lookup/`.

---

## Code layout

| Path                                     | Responsibility                                                                                                                                                                                                       |
| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mcp/dealer-lookup/index.ts`             | Server bootstrap: Express app, security middleware (IP allowlist, rate limit, bearer token), `McpServer` construction + `instructions`, the live-data tool registrations, `/health`, and `registerDocTools(server)`. |
| `mcp/dealer-lookup/tools/docs.ts`        | The documentation tool family and the pure functions behind it (`walkDocs`, `extractMeta`, `listDocs`, `searchDocs`, `getDoc`, `supportOverview`, `developerOverview`) plus `registerDocTools`.                      |
| `mcp/dealer-lookup/lib/mcp-response.ts`  | Shared response wrappers `jsonResponse(data)` and `errorResponse(message)`.                                                                                                                                          |
| `mcp/dealer-lookup/__tests__/`           | Jest tests (`index.test.ts`, `docs.test.ts`).                                                                                                                                                                        |
| `mcp/dealer-lookup/ecosystem.config.cjs` | PM2 process config (`amsoil-dlp-mcp`).                                                                                                                                                                               |

---

## Tool families

All tools are **read-only** - no tool writes to the database or mutates Stripe. Preserve that invariant.

**Live data** (registered in `index.ts`, backed by Prisma + Stripe):

- `search_dealers`, `get_dealer`, `search_users`, `get_user`, `get_stripe_info`

**Documentation** (registered by `registerDocTools` in `tools/docs.ts`, backed by the `docs/` tree):

- `support_overview` → returns `SUPPORT_OVERVIEW.md` (support runbook entry point)
- `developer_overview` → returns `DEVELOPER_OVERVIEW.md` (developer runbook entry point)
- `list_docs` → all docs with title + first paragraph (`scope: support | all`)
- `search_docs` → keyword search across docs (`scope: support | all`)
- `get_doc` → full content of one doc by relative path

The docs directory is resolved by `resolveDocsDir()` - `MCP_DOCS_DIR` if set, else `<repo>/docs`. `list_docs`/`search_docs` exclude `docs/archive/` and `docs/plans/` unless `scope: "all"` (see `EXCLUDE_DIRS` in `tools/docs.ts`).

`get_doc` is path-hardened: absolute paths rejected, `..` traversal rejected, resolved + `realpath` must stay within the docs root (symlink escape rejected), `.md` only. Keep these checks if you touch it.

---

## How to add a tool

Tools are registered with `server.tool(name, description, zodSchema, handler)` and should return through the shared wrappers:

```ts
import { z } from 'zod';
import { jsonResponse, errorResponse } from '../lib/mcp-response';

server.tool(
  'my_tool',
  'One-sentence description the model reads to decide when to call this.',
  { id: z.string().min(1).describe('Dealer cuid') },
  async ({ id }) => {
    const result = await doReadOnlyThing(id);
    if (!result.ok) return errorResponse(result.error);
    return jsonResponse(result);
  }
);
```

Guidelines:

- **Keep it read-only.** This server's value (and its security posture) depends on never mutating.
- **Group related tools behind a registrar.** The docs tools live behind `registerDocTools(server, opts)` so they can be unit-tested with an injected `docsDir`. Follow that pattern for a new family rather than piling everything into `index.ts`.
- **Put logic in pure functions, register thin handlers.** `tools/docs.ts` keeps `getDoc`/`searchDocs`/etc. as plain functions and the `server.tool(...)` handlers just wrap them - that's what makes them testable without standing up a server.
- **Update the server `instructions`** block in `index.ts` if the new tool changes how an agent should approach the server (the instructions are the model's first-read map of the tool families).

### Adding a curated-overview tool

`developer_overview` is the reference example - it mirrors `support_overview`:

1. A pure function (`developerOverview(rootDir)`) that calls `getDoc(rootDir, 'DEVELOPER_OVERVIEW.md')` and rewrites the not-found error into a helpful message.
2. A `server.tool('developer_overview', ...)` registration inside `registerDocTools`.
3. Tests in `__tests__/docs.test.ts` covering present + missing file.

---

## Testing & running locally

```bash
# from mcp/dealer-lookup (or the repo Jest runner)
npm test

# run the server locally against the repo docs tree, from the repo root
# (runs the TypeScript entrypoint directly via tsx — no build step; this is
#  exactly how PM2 starts it in production, see ecosystem.config.cjs).
# $(git rev-parse --show-toplevel) keeps MCP_DOCS_DIR correct regardless of CWD.
REPO_ROOT="$(git rev-parse --show-toplevel)"
MCP_PORT=3010 MCP_DOCS_DIR="$REPO_ROOT/docs" npx tsx "$REPO_ROOT/mcp/dealer-lookup/index.ts"
```

(The server needs `DATABASE_URL`, `STRIPE_SECRET_KEY`, and `MCP_AUTH_TOKEN` to boot - it loads them from `.env.local`/`.env` via its dotenv block. The doc tools themselves need only `MCP_DOCS_DIR`.)

Test patterns to follow (`__tests__/docs.test.ts`): the doc functions are tested against a temp directory created with `mkdtempSync`, so they need no DB or network. Pass an explicit `docsDir` (or write fixture files into the temp dir) rather than relying on the real `docs/`.

`/health` is unauthenticated and verifies DB connectivity - useful as a liveness probe and for confirming the process is up before debugging auth.

---

## Related

- [MCP_SERVER.md](./MCP_SERVER.md) - deploy, security, token rotation, troubleshooting
- [MCP_CONFIGURATION.md](./MCP_CONFIGURATION.md) - client `.mcp.json` setup
- [DEVELOPER_OVERVIEW.md](./DEVELOPER_OVERVIEW.md) - developer onboarding entry point
