home / skills / outfitter-dev / agents / stack-templates
This skill scaffolds Outfitter components by generating handler, CLI, MCP tool, and daemon templates to accelerate project bootstrapping.
npx playbooks add skill outfitter-dev/agents --skill stack-templatesReview the files below or copy the command above to add this skill to your agents.
---
name: stack-templates
version: 0.1.0
description: Templates for creating handlers, CLI commands, MCP tools, and daemon services following Outfitter Stack conventions. Use when scaffolding new components, creating handlers, adding commands, or when "create handler", "new command", "add tool", "scaffold", "template", or "daemon service" are mentioned.
context: fork
agent: stacker
allowed-tools: Read Write Edit Glob Grep
argument-hint: [component type]
---
# Stack Templates
Templates for creating @outfitter/* components.
## Component Types
| Type | Package | Template |
|------|---------|----------|
| Handler | `@outfitter/contracts` | [handler](#handler) |
| Handler Test | `@outfitter/testing` | [handler-test](#handler-test) |
| CLI Command | `@outfitter/cli` | [cli-command](#cli-command) |
| MCP Tool | `@outfitter/mcp` | [mcp-tool](#mcp-tool) |
| Daemon | `@outfitter/daemon` | [daemon-service](#daemon-service) |
## Handler
Transport-agnostic business logic returning `Result<T, E>`:
```typescript
import {
Result,
ValidationError,
NotFoundError,
createValidator,
type Handler,
} from "@outfitter/contracts";
import { z } from "zod";
// 1. Input schema
const InputSchema = z.object({
id: z.string().min(1),
});
type Input = z.infer<typeof InputSchema>;
// 2. Output type
interface Output {
id: string;
name: string;
}
// 3. Validator
const validateInput = createValidator(InputSchema);
// 4. Handler
export const myHandler: Handler<unknown, Output, ValidationError | NotFoundError> = async (
rawInput,
ctx
) => {
const inputResult = validateInput(rawInput);
if (inputResult.isErr()) return inputResult;
const input = inputResult.value;
ctx.logger.debug("Processing", { id: input.id });
const resource = await fetchResource(input.id);
if (!resource) {
return Result.err(new NotFoundError("resource", input.id));
}
return Result.ok(resource);
};
```
## Handler Test
Test handlers directly without transport layer:
```typescript
import { describe, test, expect } from "bun:test";
import { createContext } from "@outfitter/contracts";
import { myHandler } from "../handlers/my-handler.js";
describe("myHandler", () => {
test("returns success for valid input", async () => {
const ctx = createContext({});
const result = await myHandler({ id: "valid-id" }, ctx);
expect(result.isOk()).toBe(true);
expect(result.value).toMatchObject({ id: "valid-id" });
});
test("returns NotFoundError for missing resource", async () => {
const ctx = createContext({});
const result = await myHandler({ id: "missing" }, ctx);
expect(result.isErr()).toBe(true);
expect(result.error._tag).toBe("NotFoundError");
expect(result.error.resourceId).toBe("missing");
});
test("returns ValidationError for invalid input", async () => {
const ctx = createContext({});
const result = await myHandler({ id: "" }, ctx);
expect(result.isErr()).toBe(true);
expect(result.error._tag).toBe("ValidationError");
});
});
```
## CLI Command
Commander.js command calling a handler:
```typescript
import { command, output, exitWithError } from "@outfitter/cli";
import { createContext } from "@outfitter/contracts";
import { myHandler } from "../handlers/my-handler.js";
export const myCommand = command("my-command")
.description("What this command does")
.argument("<id>", "Resource ID")
.option("-l, --limit <n>", "Limit results", parseInt)
.action(async ({ args, flags }) => {
const ctx = createContext({});
const result = await myHandler({ id: args.id, limit: flags.limit }, ctx);
if (result.isErr()) {
exitWithError(result.error);
}
await output(result.value);
})
.build();
```
Register in CLI:
```typescript
import { createCLI } from "@outfitter/cli";
import { myCommand } from "./commands/my-command.js";
const cli = createCLI({ name: "myapp", version: "1.0.0" });
cli.program.addCommand(myCommand);
cli.program.parse();
```
## MCP Tool
Use `defineTool()` for type-safe tool definitions with automatic schema inference:
```typescript
import { defineTool } from "@outfitter/mcp";
import { Result, ValidationError } from "@outfitter/contracts";
import { z } from "zod";
const InputSchema = z.object({
query: z.string().describe("Search query"),
limit: z.number().int().positive().default(10).describe("Max results"),
});
interface Output {
results: Array<{ id: string; title: string }>;
total: number;
}
export const myTool = defineTool({
name: "my_tool",
description: "Tool description for AI agent",
inputSchema: InputSchema,
handler: async (input): Promise<Result<Output, ValidationError>> => {
// input is automatically typed as z.infer<typeof InputSchema>
const results = await search(input.query, input.limit);
return Result.ok({ results, total: results.length });
},
});
```
Register in server:
```typescript
import { createMcpServer } from "@outfitter/mcp";
import { myTool } from "./tools/my-tool.js";
const server = createMcpServer({ name: "my-server", version: "0.1.0" });
server.registerTool(myTool);
server.start();
```
## Daemon Service
Background service with health checks and IPC:
```typescript
import {
createDaemon,
createIpcServer,
createHealthChecker,
getSocketPath,
getLockPath,
} from "@outfitter/daemon";
import { createLogger, createConsoleSink } from "@outfitter/logging";
import { Result } from "@outfitter/contracts";
const logger = createLogger({
name: "my-daemon",
level: "info",
sinks: [createConsoleSink()],
redaction: { enabled: true },
});
const daemon = createDaemon({
name: "my-daemon",
pidFile: getLockPath("my-daemon"),
logger,
shutdownTimeout: 10000,
});
const healthChecker = createHealthChecker([
{
name: "memory",
check: async () => {
const used = process.memoryUsage().heapUsed / 1024 / 1024;
return used < 500
? Result.ok(undefined)
: Result.err(new Error(`High memory: ${used.toFixed(2)}MB`));
},
},
]);
const ipcServer = createIpcServer(getSocketPath("my-daemon"));
ipcServer.onMessage(async (msg) => {
const message = msg as { type: string };
switch (message.type) {
case "status": return { status: "ok", uptime: process.uptime() };
case "health": return await healthChecker.check();
default: return { error: "Unknown command" };
}
});
daemon.onShutdown(async () => {
logger.info("Shutting down...");
await ipcServer.close();
});
async function main() {
const startResult = await daemon.start();
if (startResult.isErr()) {
logger.error("Failed to start", { error: startResult.error });
process.exit(1);
}
await ipcServer.listen();
logger.info("Started", { socket: getSocketPath("my-daemon") });
}
main();
```
## Best Practices
1. **Handler First** - Write handler before adapter (CLI/MCP/API)
2. **Validate Early** - Use `createValidator` at handler entry
3. **Type Errors** - List all error types in handler signature
4. **Context Propagation** - Pass context through all handler calls
5. **Test Handlers** - Test handlers directly without transport layer
## References
- [templates/handler.md](templates/handler.md)
- [templates/handler-test.md](templates/handler-test.md)
- [templates/cli-command.md](templates/cli-command.md)
- [templates/mcp-tool.md](templates/mcp-tool.md)
- [templates/daemon-service.md](templates/daemon-service.md)
This skill provides ready-made TypeScript templates for creating handlers, CLI commands, MCP tools, and daemon services that follow Outfitter Stack conventions. It helps scaffold transport-agnostic business logic, tests, command wiring, agent tools, and background services with recommended patterns. Use the templates to ensure consistent input validation, error handling, context propagation, and testability across components.
Templates supply minimal, opinionated code examples: a handler pattern that returns Result<T, E> with zod validation, a handler test harness that runs handlers without adapters, a Commander.js CLI command wrapper, a type-safe MCP tool using defineTool, and a daemon service with IPC and health checks. Each template shows imports, input schemas, output types, error handling, context creation, and registration with the runtime.
Do handlers depend on transport libraries?
No. Handlers are transport-agnostic and return Result<T, E>, so you can call them from CLI, MCP, HTTP adapters, or tests.
How do I validate inputs?
Use zod to define an InputSchema and createValidator to validate raw input at handler entry, returning a ValidationError result when invalid.