Skip to main content
Glama
README.md
# harness-mcp

An opinionated TypeScript boilerplate for building **MCP servers** with **harness engineering** in mind.

> Harness engineering is the discipline of designing the scaffolding around an LLM agent — tools, descriptions, errors, context — so the agent actually does the right thing. Most MCP boilerplates teach you the protocol. This one teaches you the protocol _and_ the practice.

## What's in the box

- **`defineTool()`** — one Zod schema feeds the MCP SDK, OpenAI's function-calling API, and the runtime handler. Validation and error wrapping are automatic.
- **Both transports** — stdio (`src/index.ts`) for Claude Code-style local clients, Streamable HTTP (`src/http.ts`) for remote/web clients. Both share one `createServer()`.
- **Structured `AgentError`s** — every error has a `code`, a `message`, and a `hint` written for the model: "call `items_list` first to find a valid id." Vague errors waste turns; this fixes that at the type level.
- **A real eval harness** — Vitest-based. Unit tests run free in CI; `tests/mcp/echo.test.ts` drives a real OpenAI model through the MCP server via an in-memory transport pair and asserts on the resulting tool-call trace.
- **A simple CRUD example** — `items_create / list / read / update / delete` plus an `echo` tool. Replace the in-memory store with your real backend; keep the shape.

## Quick start

```bash
bun install
bun test              # unit tests, no API key needed
bun run start         # stdio server on stdin/stdout
bun run start:http    # HTTP server on http://localhost:3000/mcp
```

To run the model-in-the-loop evals:

```bash
cp .env.example .env
# add OPENAI_API_KEY
bun run test:mcp
```

## Wire into Claude Code

```json
{
  "mcpServers": {
    "harness-mcp": {
      "command": "bun",
      "args": ["run", "/absolute/path/to/harness/mcp/src/index.ts"]
    }
  }
}
```

## Layout

```
src/
  index.ts              stdio entry
  http.ts               streamable-http entry
  core/
    server.ts           createServer() — shared by both transports
    tool.ts             defineTool() wrapper
    errors.ts           AgentError
    store.ts            replace with your backend
  tools/
    echo.ts             smoke-test tool
    items-*.ts          CRUD example tools
    index.ts            registry

tests/
  unit/                 fast, no API key
    tool.test.ts
    store.test.ts
  mcp/                  protocol + model-in-the-loop
    setup.ts            in-memory client + runWithModel() helper
    smoke.test.ts       no model
    echo.test.ts        gpt-4o-mini, skipped without OPENAI_API_KEY
```

## The opinions

1. **Tool descriptions are prompt engineering.** Every description leads with `USE WHEN ...` and includes `DO NOT USE WHEN ...` for sibling tools the model could confuse this with. The smoke test enforces the convention.
2. **Errors teach.** Every `AgentError` carries a `hint` field. Read your error messages as if you were the agent — would you know what to do next? If not, rewrite.
3. **List endpoints paginate.** `items_list` returns `{ items, nextCursor }`. Default limit 20, hard cap 100. Don't dump unbounded data into the context.
4. **Destructive ops accept `dryRun`.** `items_delete` will tell you what _would_ happen if you weren't sure.
5. **One Zod, three consumers.** Don't maintain JSON Schema by hand alongside Zod — `defineTool` derives both.
6. **Evals are tests.** Tool-call traces are assertable. When a description regression breaks the model's behavior, your test catches it.

## License

MIT.