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-templates

Review the files below or copy the command above to add this skill to your agents.

Files (6)
SKILL.md
7.2 KB
---
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)

Overview

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.

How this skill works

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.

When to use it

  • Scaffolding a new transport-agnostic handler for business logic
  • Adding unit tests that exercise handlers directly without infrastructure
  • Creating a new CLI command that calls a handler
  • Defining an MCP tool for AI agents with automatic schema inference
  • Implementing a background daemon with IPC and health checks

Best practices

  • Write the handler first, then add adapters (CLI/MCP/API)
  • Validate inputs at handler entry using createValidator and zod
  • List all possible error types in the handler signature
  • Propagate a context object through handler calls for logging and tracing
  • Test handlers directly with the provided handler-test pattern

Example use cases

  • Implement a resource lookup handler that returns NotFoundError when absent
  • Scaffold a CLI command that parses args/options and delegates to a handler
  • Create an MCP tool with inputSchema for typed AI agent interactions
  • Build a daemon that exposes status and health via IPC and performs graceful shutdown
  • Write handler unit tests to validate success, validation failures, and domain errors

FAQ

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.