Outfitter Atlas
Your trail map for building with @outfitter/* packages. This guide covers the patterns, the templates, and—just as importantly—the *why* behind it all.
Why We Built This
We kept solving the same problems across projects: config loading, error handling, CLI output modes, MCP server boilerplate. Every tool needed the same foundation. So we extracted it.
Agents Are the New Users
The patterns assume you're building tools that agents will consume—structured output, typed errors, predictable behavior. Humans benefit too; agents just make the stakes clearer.
When an AI agent calls your CLI or MCP tool, it needs:
- Structured output it can parse (JSON when explicitly requested)
- Typed errors with categories it can reason about (retry? abort? ask user?)
- Predictable exit codes for scripting and automation
- Consistent behavior across transport surfaces
Outfitter enforces these properties by design, not discipline.
Errors Are Data, Not Exceptions
Traditional error handling (throw/catch) loses context, breaks type safety, and makes control flow unpredictable. We treat errors as first-class data:
const error = NotFoundError.create("user", "user-123");
error._tag; // "NotFoundError" — for pattern matching
error.category; // "not_found" — maps to exit code 2, HTTP 404
error.message; // Human-readable
error.toJSON(); // Serializes cleanly for agents
Ten categories cover all failure modes. Each maps to exit codes (CLI) and HTTP status (API/MCP). Agents can make retry decisions without parsing error strings.
Tests First, Always
Tests define behavior; implementations follow. The workflow:
- Red: Write a failing test that defines expected behavior
- Green: Minimal code to pass
- Refactor: Improve while staying green
The test proves the behavior exists. A failing test proves it doesn't—yet.
One Definition, Many Derivations
Types, schemas, and contracts have exactly one source:
| Concern |
Source of Truth |
Derives |
| Input validation |
Zod schema |
TypeScript types, JSON Schema |
| Error categories |
ErrorCategory type |
Exit codes, HTTP status |
| CLI flag names |
Handler input types |
MCP tool parameters |
If two things must stay in sync, one derives from the other.
Bun-Native When Possible
Bun APIs before npm packages—faster, zero-dependency:
| Need |
Use |
| Hashing |
Bun.hash() |
| Globbing |
Bun.Glob |
| Semver |
Bun.semver |
| Shell |
Bun.$ |
| SQLite |
bun:sqlite |
| UUID v7 |
Bun.randomUUIDv7() |
The Core Idea
Handlers are pure functions returning Result<T, E>. CLI and MCP are thin adapters over the same logic. Write the handler once, expose it everywhere.
type Handler<TInput, TOutput, TError extends OutfitterError> = (
input: TInput,
ctx: HandlerContext
) => Promise<Result<TOutput, TError>>;
This buys you:
- Testability — Call the function directly, no transport mocking
- Reusability — Same handler serves CLI, MCP, HTTP
- Type Safety — Input, output, and error types are explicit
- Composability — Handlers wrap handlers
The Package Landscape
Dependencies flow one direction: Foundation → Runtime → Tooling.
┌─────────────────────────────────────────────────────────────────┐
│ TOOLING TIER │
│ @outfitter/testing @outfitter/tooling │
└─────────────────────────────────────────────────────────────────┘
▲
┌─────────────────────────────────────────────────────────────────┐
│ RUNTIME TIER │
│ @outfitter/cli @outfitter/mcp @outfitter/daemon │
│ @outfitter/config @outfitter/logging @outfitter/file-ops @outfitter/tui │
│ @outfitter/state @outfitter/index @outfitter/schema │
└─────────────────────────────────────────────────────────────────┘
▲
┌─────────────────────────────────────────────────────────────────┐
│ FOUNDATION TIER │
│ @outfitter/contracts @outfitter/types │
└─────────────────────────────────────────────────────────────────┘
| Package |
What It Does |
Reach For When... |
@outfitter/contracts |
Result types, errors, Handler contract |
Always. This is the foundation. |
@outfitter/types |
Type utilities, collection helpers |
You need type manipulation |
@outfitter/cli |
Commands, output modes, formatting |
Building CLI applications |
@outfitter/mcp |
Server framework, tool registration |
Building AI agent tools |
@outfitter/config |
XDG paths, config loading |
You have configuration |
@outfitter/logging |
Structured logging, redaction |
You need logging (you do) |
@outfitter/daemon |
Lifecycle, IPC, health checks |
Building background services |
@outfitter/file-ops |
Atomic writes, locking, secure paths |
File operations that matter |
@outfitter/state |
Pagination, cursor state |
Paginated data |
@outfitter/index |
SQLite FTS5, WAL mode, BM25 ranking |
Full-text search indexing |
@outfitter/schema |
Schema introspection, surface maps, drift detection |
CLI/MCP parity docs and CI drift checks |
@outfitter/tui |
Terminal UI rendering primitives and prompts |
Rich terminal UX (tables, trees, prompts, streaming) |
@outfitter/testing |
Test harnesses, fixtures |
Testing (always) |
@outfitter/tooling |
oxlint, TypeScript, Lefthook presets |
Project setup (dev dependency) |
Trail Map: Designing a System
Five things to know when building with Outfitter. For the complete design process with templates, see guides/architecture.md.
1. Know Your Terrain
Before writing code, understand:
- Transport surfaces — CLI, MCP, HTTP, or all three?
- Domain operations — What actions does the system perform?
- Failure modes — What can go wrong? (these map to error taxonomy)
- External dependencies — APIs, databases, file system?
2. Design the Handler Layer
For each domain operation:
- Define input type (Zod schema)
- Define output type
- Identify error types (from taxonomy)
- Write the signature:
Handler<Input, Output, Error1 | Error2>
const CreateUserInputSchema = z.object({
email: z.string().email(),
name: z.string().min(1),
});
interface User {
id: string;
email: string;
name: string;
}
const createUser: Handler<unknown, User, ValidationError | ConflictError>;
3. Map Errors to the Taxonomy
Ten categories. Memorize the exit codes—you'll use them.
| Category |
Exit |
HTTP |
Class |
When |
validation |
1 |
400 |
ValidationError |
Bad input, schema failures |
not_found |
2 |
404 |
NotFoundError |
Resource doesn't exist |
conflict |
3 |
409 |
AlreadyExistsError |
Resource already exists |
conflict |
3 |
409 |
ConflictError |
Version mismatch, concurrent modification |
permission |
4 |
403 |
PermissionError |
Forbidden action |
timeout |
5 |
504 |
TimeoutError |
Took too long |
rate_limit |
6 |
429 |
RateLimitError |
Too many requests |
network |
7 |
502 |
NetworkError |
Connection failures |
internal |
8 |
500 |
InternalError |
Bugs, unexpected errors |
auth |
9 |
401 |
AuthError |
Authentication required |
cancelled |
130 |
499 |
CancelledError |
User hit Ctrl+C |
4. Pick Your Packages
Start with @outfitter/contracts. Always.
- Building a CLI? Add
@outfitter/cli
- Building MCP tools? Add
@outfitter/mcp
- Touching files? Add
@outfitter/config (paths) + @outfitter/file-ops (safety)
5. Wire Up Context Flow
Decide:
- Entry points — Where does context get created?
- What's in context — Logger, config, signal, workspaceRoot
- Tracing — How does requestId flow through?
Guides
Deeper dives into specific topics.
| Guide |
What's Covered |
Location |
| Getting Started |
First handler, CLI + MCP adapters |
guides/getting-started.md |
| Architecture Design |
5-step process, templates, constraints |
guides/architecture.md |
Pattern Deep Dives
When you need the details, not just the overview.
| Pattern |
What's Covered |
Location |
| Handler Contract |
Input, context, Result |
patterns/handler.md |
| Error Taxonomy |
10 categories, exit/HTTP mapping |
patterns/errors.md |
| Result Utilities |
Creating, checking, transforming |
patterns/results.md |
| CLI Patterns |
Commands, output modes, pagination |
patterns/cli.md |
| MCP Patterns |
Tools, resources, prompts |
patterns/mcp.md |
| Daemon Patterns |
Lifecycle, IPC, health checks |
patterns/daemon.md |
| File Operations |
Atomic writes, locking, paths |
patterns/file-ops.md |
| Logging |
Structured logging, redaction |
patterns/logging.md |
| Testing |
Harnesses, fixtures, mocks |
patterns/testing.md |
| Converting Code |
Migrating to Outfitter conventions |
patterns/conversion.md |
| Schema Introspection |
Manifest generation, surface maps, drift detection |
patterns/schema.md |
Templates: Just Add Code
Copy, paste, customize.
| Template |
For |
Location |
| Handler |
Transport-agnostic business logic |
templates/handler.md |
| Handler Test |
Testing handlers with Bun |
templates/handler-test.md |
| CLI Command |
Commander.js wrapper |
templates/cli-command.md |
| MCP Tool |
Zod-schema tool definition |
templates/mcp-tool.md |
| Daemon Service |
Background service with IPC |
templates/daemon-service.md |
Quick Start
# Foundation first
bun add @outfitter/contracts
# Then what you need
bun add @outfitter/cli # CLI apps
bun add @outfitter/mcp # MCP servers
bun add @outfitter/logging # Structured logging
bun add @outfitter/config # XDG-compliant config
bun add @outfitter/index # Full-text search
# Dev dependencies
bun add -D @outfitter/testing @outfitter/tooling
New here? Start with guides/getting-started.md.
Command Canon
Use canonical command forms when referencing repo maintenance workflows:
outfitter repo check <docs|exports|readme|registry|changeset|tree|boundary-invocations>
outfitter repo sync docs
outfitter repo export docs
Do not use removed legacy aliases such as outfitter docs <sync|check|export>,
outfitter repo docs-sync, or outfitter repo check-exports.
The Rules
Do
- Use Result types, not exceptions
- Map domain errors to taxonomy categories
- Design handlers as pure functions:
(input, ctx) => Result
- Include error types in handler signatures
- Validate at handler entry with
createValidator
- Pass context through all handler calls
- Test handlers directly—no transport layer needed
Don't
- Throw exceptions in handlers
- Put transport-specific logic in handlers
- Hardcode paths (use XDG via
@outfitter/config)
- Skip error type planning
- Couple handlers to specific transports
- Use
console.log (use ctx.logger)
Troubleshooting
Something not working? Use debug-outfitter for systematic investigation with structured reports. Common issues:
- Result always error — Check for missing
await on async handlers
- Type narrowing broken — Don't reassign Result variables after checking
- MCP tool not appearing — Register before
start(), add .describe() to schema fields
- Wrong exit code — Use
exitWithError(), not process.exit()
If the issue is in Outfitter itself, use outfitter-issue to file a bug.
1---2name: outfitter-atlas3description: Generates patterns, templates, and guides for @outfitter/* packages. Covers transport-agnostic handler systems, Result types, error taxonomy, and package APIs. Use when working with @outfitter/*, Result types, Handler contract, error taxonomy, or when Result, Handler, ValidationError, NotFoundError, OutfitterError, or package names like contracts, cli, mcp, schema, tui, daemon, config, logging are mentioned.4---56# Outfitter Atlas78Your trail map for building with @outfitter/* packages. This guide covers the patterns, the templates, and—just as importantly—the *why\* behind it all.910## Why We Built This1112We kept solving the same problems across projects: config loading, error handling, CLI output modes, MCP server boilerplate. Every tool needed the same foundation. So we extracted it.1314### Agents Are the New Users1516The patterns assume you're building tools that **agents will consume**—structured output, typed errors, predictable behavior. Humans benefit too; agents just make the stakes clearer.1718When an AI agent calls your CLI or MCP tool, it needs:1920- **Structured output** it can parse (JSON when explicitly requested)21- **Typed errors** with categories it can reason about (retry? abort? ask user?)22- **Predictable exit codes** for scripting and automation23- **Consistent behavior** across transport surfaces2425Outfitter enforces these properties by design, not discipline.2627### Errors Are Data, Not Exceptions2829Traditional error handling (`throw`/`catch`) loses context, breaks type safety, and makes control flow unpredictable. We treat errors as first-class data:3031```typescript32const error = NotFoundError.create("user", "user-123");33error._tag; // "NotFoundError" — for pattern matching34error.category; // "not_found" — maps to exit code 2, HTTP 40435error.message; // Human-readable36error.toJSON(); // Serializes cleanly for agents37```3839Ten categories cover all failure modes. Each maps to exit codes (CLI) and HTTP status (API/MCP). Agents can make retry decisions without parsing error strings.4041### Tests First, Always4243Tests define behavior; implementations follow. The workflow:44451. **Red**: Write a failing test that defines expected behavior462. **Green**: Minimal code to pass473. **Refactor**: Improve while staying green4849The test proves the behavior exists. A failing test proves it doesn't—_yet_.5051### One Definition, Many Derivations5253Types, schemas, and contracts have exactly one source:5455| Concern | Source of Truth | Derives |56| ---------------- | -------------------- | ----------------------------- |57| Input validation | Zod schema | TypeScript types, JSON Schema |58| Error categories | `ErrorCategory` type | Exit codes, HTTP status |59| CLI flag names | Handler input types | MCP tool parameters |6061If two things must stay in sync, one derives from the other.6263### Bun-Native When Possible6465Bun APIs before npm packages—faster, zero-dependency:6667| Need | Use |68| -------- | -------------------- |69| Hashing | `Bun.hash()` |70| Globbing | `Bun.Glob` |71| Semver | `Bun.semver` |72| Shell | `Bun.$` |73| SQLite | `bun:sqlite` |74| UUID v7 | `Bun.randomUUIDv7()` |7576## The Core Idea7778**Handlers are pure functions returning `Result<T, E>`.** CLI and MCP are thin adapters over the same logic. Write the handler once, expose it everywhere.7980```typescript81type Handler<TInput, TOutput, TError extends OutfitterError> = (82 input: TInput,83 ctx: HandlerContext84) => Promise<Result<TOutput, TError>>;85```8687This buys you:8889- **Testability** — Call the function directly, no transport mocking90- **Reusability** — Same handler serves CLI, MCP, HTTP91- **Type Safety** — Input, output, and error types are explicit92- **Composability** — Handlers wrap handlers9394## The Package Landscape9596Dependencies flow one direction: Foundation → Runtime → Tooling.9798```99┌─────────────────────────────────────────────────────────────────┐100│ TOOLING TIER │101│ @outfitter/testing @outfitter/tooling │102└─────────────────────────────────────────────────────────────────┘103 ▲104┌─────────────────────────────────────────────────────────────────┐105│ RUNTIME TIER │106│ @outfitter/cli @outfitter/mcp @outfitter/daemon │107│ @outfitter/config @outfitter/logging @outfitter/file-ops @outfitter/tui │108│ @outfitter/state @outfitter/index @outfitter/schema │109└─────────────────────────────────────────────────────────────────┘110 ▲111┌─────────────────────────────────────────────────────────────────┐112│ FOUNDATION TIER │113│ @outfitter/contracts @outfitter/types │114└─────────────────────────────────────────────────────────────────┘115116```117118| Package | What It Does | Reach For When... |119| ---------------------- | --------------------------------------------------- | ---------------------------------------------------- |120| `@outfitter/contracts` | Result types, errors, Handler contract | Always. This is the foundation. |121| `@outfitter/types` | Type utilities, collection helpers | You need type manipulation |122| `@outfitter/cli` | Commands, output modes, formatting | Building CLI applications |123| `@outfitter/mcp` | Server framework, tool registration | Building AI agent tools |124| `@outfitter/config` | XDG paths, config loading | You have configuration |125| `@outfitter/logging` | Structured logging, redaction | You need logging (you do) |126| `@outfitter/daemon` | Lifecycle, IPC, health checks | Building background services |127| `@outfitter/file-ops` | Atomic writes, locking, secure paths | File operations that matter |128| `@outfitter/state` | Pagination, cursor state | Paginated data |129| `@outfitter/index` | SQLite FTS5, WAL mode, BM25 ranking | Full-text search indexing |130| `@outfitter/schema` | Schema introspection, surface maps, drift detection | CLI/MCP parity docs and CI drift checks |131| `@outfitter/tui` | Terminal UI rendering primitives and prompts | Rich terminal UX (tables, trees, prompts, streaming) |132| `@outfitter/testing` | Test harnesses, fixtures | Testing (always) |133| `@outfitter/tooling` | oxlint, TypeScript, Lefthook presets | Project setup (dev dependency) |134135## Trail Map: Designing a System136137Five things to know when building with Outfitter. For the complete design process with templates, see [guides/architecture.md](${CLAUDE_PLUGIN_ROOT}/shared/guides/architecture.md).138139### 1. Know Your Terrain140141Before writing code, understand:142143- **Transport surfaces** — CLI, MCP, HTTP, or all three?144- **Domain operations** — What actions does the system perform?145- **Failure modes** — What can go wrong? (these map to error taxonomy)146- **External dependencies** — APIs, databases, file system?147148### 2. Design the Handler Layer149150For each domain operation:1511521. Define input type (Zod schema)1532. Define output type1543. Identify error types (from taxonomy)1554. Write the signature: `Handler<Input, Output, Error1 | Error2>`156157```typescript158const CreateUserInputSchema = z.object({159 email: z.string().email(),160 name: z.string().min(1),161});162163interface User {164 id: string;165 email: string;166 name: string;167}168169const createUser: Handler<unknown, User, ValidationError | ConflictError>;170```171172### 3. Map Errors to the Taxonomy173174Ten categories. Memorize the exit codes—you'll use them.175176| Category | Exit | HTTP | Class | When |177| ------------ | ---- | ---- | -------------------- | ----------------------------------------- |178| `validation` | 1 | 400 | `ValidationError` | Bad input, schema failures |179| `not_found` | 2 | 404 | `NotFoundError` | Resource doesn't exist |180| `conflict` | 3 | 409 | `AlreadyExistsError` | Resource already exists |181| `conflict` | 3 | 409 | `ConflictError` | Version mismatch, concurrent modification |182| `permission` | 4 | 403 | `PermissionError` | Forbidden action |183| `timeout` | 5 | 504 | `TimeoutError` | Took too long |184| `rate_limit` | 6 | 429 | `RateLimitError` | Too many requests |185| `network` | 7 | 502 | `NetworkError` | Connection failures |186| `internal` | 8 | 500 | `InternalError` | Bugs, unexpected errors |187| `auth` | 9 | 401 | `AuthError` | Authentication required |188| `cancelled` | 130 | 499 | `CancelledError` | User hit Ctrl+C |189190### 4. Pick Your Packages191192Start with `@outfitter/contracts`. Always.193194- Building a CLI? Add `@outfitter/cli`195- Building MCP tools? Add `@outfitter/mcp`196- Touching files? Add `@outfitter/config` (paths) + `@outfitter/file-ops` (safety)197198### 5. Wire Up Context Flow199200Decide:201202- **Entry points** — Where does context get created?203- **What's in context** — Logger, config, signal, workspaceRoot204- **Tracing** — How does requestId flow through?205206## Guides207208Deeper dives into specific topics.209210| Guide | What's Covered | Location |211| ----------------------- | -------------------------------------- | ----------------------------------------------------------------------------------- |212| **Getting Started** | First handler, CLI + MCP adapters | [guides/getting-started.md](${CLAUDE_PLUGIN_ROOT}/shared/guides/getting-started.md) |213| **Architecture Design** | 5-step process, templates, constraints | [guides/architecture.md](${CLAUDE_PLUGIN_ROOT}/shared/guides/architecture.md) |214215## Pattern Deep Dives216217When you need the details, not just the overview.218219| Pattern | What's Covered | Location |220| ------------------------ | -------------------------------------------------- | ----------------------------------------------------------------------------- |221| **Handler Contract** | Input, context, Result | [patterns/handler.md](${CLAUDE_PLUGIN_ROOT}/shared/patterns/handler.md) |222| **Error Taxonomy** | 10 categories, exit/HTTP mapping | [patterns/errors.md](${CLAUDE_PLUGIN_ROOT}/shared/patterns/errors.md) |223| **Result Utilities** | Creating, checking, transforming | [patterns/results.md](${CLAUDE_PLUGIN_ROOT}/shared/patterns/results.md) |224| **CLI Patterns** | Commands, output modes, pagination | [patterns/cli.md](${CLAUDE_PLUGIN_ROOT}/shared/patterns/cli.md) |225| **MCP Patterns** | Tools, resources, prompts | [patterns/mcp.md](${CLAUDE_PLUGIN_ROOT}/shared/patterns/mcp.md) |226| **Daemon Patterns** | Lifecycle, IPC, health checks | [patterns/daemon.md](${CLAUDE_PLUGIN_ROOT}/shared/patterns/daemon.md) |227| **File Operations** | Atomic writes, locking, paths | [patterns/file-ops.md](${CLAUDE_PLUGIN_ROOT}/shared/patterns/file-ops.md) |228| **Logging** | Structured logging, redaction | [patterns/logging.md](${CLAUDE_PLUGIN_ROOT}/shared/patterns/logging.md) |229| **Testing** | Harnesses, fixtures, mocks | [patterns/testing.md](${CLAUDE_PLUGIN_ROOT}/shared/patterns/testing.md) |230| **Converting Code** | Migrating to Outfitter conventions | [patterns/conversion.md](${CLAUDE_PLUGIN_ROOT}/shared/patterns/conversion.md) |231| **Schema Introspection** | Manifest generation, surface maps, drift detection | [patterns/schema.md](${CLAUDE_PLUGIN_ROOT}/shared/patterns/schema.md) |232233## Templates: Just Add Code234235Copy, paste, customize.236237| Template | For | Location |238| ------------------ | --------------------------------- | --------------------------------------------------------------------------------------- |239| **Handler** | Transport-agnostic business logic | [templates/handler.md](${CLAUDE_PLUGIN_ROOT}/shared/templates/handler.md) |240| **Handler Test** | Testing handlers with Bun | [templates/handler-test.md](${CLAUDE_PLUGIN_ROOT}/shared/templates/handler-test.md) |241| **CLI Command** | Commander.js wrapper | [templates/cli-command.md](${CLAUDE_PLUGIN_ROOT}/shared/templates/cli-command.md) |242| **MCP Tool** | Zod-schema tool definition | [templates/mcp-tool.md](${CLAUDE_PLUGIN_ROOT}/shared/templates/mcp-tool.md) |243| **Daemon Service** | Background service with IPC | [templates/daemon-service.md](${CLAUDE_PLUGIN_ROOT}/shared/templates/daemon-service.md) |244245## Quick Start246247```bash248# Foundation first249bun add @outfitter/contracts250251# Then what you need252bun add @outfitter/cli # CLI apps253bun add @outfitter/mcp # MCP servers254bun add @outfitter/logging # Structured logging255bun add @outfitter/config # XDG-compliant config256bun add @outfitter/index # Full-text search257258# Dev dependencies259bun add -D @outfitter/testing @outfitter/tooling260261```262263New here? Start with [guides/getting-started.md](${CLAUDE_PLUGIN_ROOT}/shared/guides/getting-started.md).264265## Command Canon266267Use canonical command forms when referencing repo maintenance workflows:268269- `outfitter repo check <docs|exports|readme|registry|changeset|tree|boundary-invocations>`270- `outfitter repo sync docs`271- `outfitter repo export docs`272273Do not use removed legacy aliases such as `outfitter docs <sync|check|export>`,274`outfitter repo docs-sync`, or `outfitter repo check-exports`.275276## The Rules277278### Do279280- Use Result types, not exceptions281- Map domain errors to taxonomy categories282- Design handlers as pure functions: `(input, ctx) => Result`283- Include error types in handler signatures284- Validate at handler entry with `createValidator`285- Pass context through all handler calls286- Test handlers directly—no transport layer needed287288### Don't289290- Throw exceptions in handlers291- Put transport-specific logic in handlers292- Hardcode paths (use XDG via `@outfitter/config`)293- Skip error type planning294- Couple handlers to specific transports295- Use `console.log` (use `ctx.logger`)296297## Troubleshooting298299Something not working? Use `debug-outfitter` for systematic investigation with structured reports. Common issues:300301- **Result always error** — Check for missing `await` on async handlers302- **Type narrowing broken** — Don't reassign Result variables after checking303- **MCP tool not appearing** — Register before `start()`, add `.describe()` to schema fields304- **Wrong exit code** — Use `exitWithError()`, not `process.exit()`305306If the issue is in Outfitter itself, use `outfitter-issue` to file a bug.