# Contributing to Pare

Thanks for your interest in contributing! Pare is designed to make contributing easy — each server is a self-contained package that wraps a single dev tool.

## Project Structure

Pare is a monorepo managed with **pnpm** and **TurboRepo**.

- `packages/shared` — Common library: dual output helpers, command runner, ANSI strip, validation
- `packages/server-*` — One package per CLI tool (e.g., `server-git`, `server-npm`, `server-cargo`)
- `packages/tsconfig`, `packages/eslint-config` — Shared configuration

```
packages/
  shared/          @paretools/shared    — Dual output helper, command runner, ANSI strip
  server-git/      @paretools/git       — Git operations (24 tools)
  server-github/   @paretools/github    — GitHub PR/issue/run operations (24 tools)
  server-docker/   @paretools/docker    — Docker & Compose operations (16 tools)
  server-python/   @paretools/python    — Python tools (14 tools)
  server-cargo/    @paretools/cargo     — Rust/Cargo tools (12 tools)
  server-go/       @paretools/go        — Go tools (11 tools)
  server-npm/      @paretools/npm       — npm/pnpm/yarn operations (10 tools)
  server-lint/     @paretools/lint      — Linters & formatters (9 tools)
  server-build/    @paretools/build     — Build tools (7 tools)
  server-k8s/      @paretools/k8s       — Kubernetes & Helm (5 tools)
  server-search/   @paretools/search    — File search & jq (4 tools)
  server-http/     @paretools/http      — HTTP requests (4 tools)
  server-test/     @paretools/test      — Test runners (3 tools)
  server-security/ @paretools/security  — Secret & vulnerability scanning (3 tools)
  server-make/     @paretools/make      — Make operations (2 tools)
  server-process/  @paretools/process   — Command execution (1 tool)
  init/            @paretools/init      — Setup CLI & health check (not an MCP server)
  tsconfig/        Shared TypeScript config
  eslint-config/   Shared ESLint config
```

## Development Setup

```bash
# Clone the repo
git clone https://github.com/Dave-London/Pare.git
cd pare

# Install dependencies
pnpm install

# Build all packages
pnpm build

# Run tests
pnpm test
```

**Requirements**: Node.js >= 20, pnpm >= 10

## Adding a Tool to an Existing Server

This is the easiest way to contribute. Follow the pattern in [`packages/server-git/src/tools/log.ts`](packages/server-git/src/tools/log.ts) as a template:

1. Create a new file in `src/tools/<command>.ts`
2. Define a `register<Command>Tool` function
3. Register the tool in `src/index.ts`

## Adding a New Server

This is the most impactful contribution. Each server wraps a CLI tool with structured, token-efficient output.

### 1. Scaffold the package

```bash
mkdir -p packages/server-<tool>/src/{tools,schemas,lib}
mkdir -p packages/server-<tool>/__tests__
```

### 2. Create `package.json`

```json
{
  "name": "@paretools/<tool>",
  "version": "0.1.0",
  "type": "module",
  "bin": { "pare-<tool>": "./dist/index.js" },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.26.0",
    "@paretools/shared": "workspace:*",
    "zod": "^4.3.6"
  }
}
```

### 3. Define output schemas (Zod)

Every tool needs a Zod schema in `src/schemas/index.ts`. Think about what data the agent actually needs — strip everything else.

```typescript
import { z } from "zod";

export const MyToolSchema = z.object({
  // Only include fields an agent would act on
  success: z.boolean(),
  errors: z.array(
    z.object({
      file: z.string(),
      line: z.number(),
      message: z.string(),
    }),
  ),
});
```

### 4. Implement the tool

Every tool MUST use `outputSchema` and return dual output:

```typescript
import { dualOutput } from "@paretools/shared";
import { MyToolSchema } from "../schemas/index.js";

server.registerTool(
  "my-command",
  {
    title: "My Tool Command",
    description: "What it does",
    inputSchema: {
      /* Zod input params */
    },
    outputSchema: MyToolSchema,
  },
  async (params) => {
    const result = await runMyCommand(params);
    return dualOutput(result, formatHumanReadable);
  },
);
```

### 5. Write tests

Use Vitest. Test the parsers with fixture data (real CLI output captured as strings).

```typescript
import { describe, it, expect } from "vitest";
import { parseMyOutput } from "../src/lib/parsers.js";

describe("parseMyOutput", () => {
  it("parses successful output", () => {
    const raw = `...real CLI output...`;
    const result = parseMyOutput(raw);
    expect(result.success).toBe(true);
  });
});
```

### 6. Create a changeset

```bash
pnpm changeset
```

Select your new package, choose `minor` version bump, and write a brief description.

## Testing

We use **Vitest** for all testing. Run tests for all packages or a specific one:

```bash
pnpm test                          # Run all tests
pnpm --filter @paretools/git test  # Run tests for a specific package
```

### Test Types

1. **Unit Tests** — Test parsers and utility functions using static fixtures (captured CLI output)
2. **Integration Tests** — Test the full MCP server lifecycle by spawning the server and using an MCP client
3. **Fidelity Tests** — Run real CLI commands and verify that the structured output accurately reflects the raw output

### Smoke Tests

Smoke tests validate the full tool chain — fixture → mock runner → tool handler → parser → schema — using real CLI output captured as fixtures. They live in `tests/smoke/` and are separate from per-package unit tests.

```bash
pnpm smoke              # Run all smoke tests (mocked + recorded)
pnpm smoke:coverage     # Show coverage report across all scenario files
```

**Directory structure:**

```
tests/smoke/
  scenarios/     # Scenario mapping files (*.md) — one per server group
  fixtures/      # Recorded CLI output (*.txt) — organized by tool
  mocked/        # Phase 2 mocked tests (*.smoke.test.ts)
  suite/         # Phase 3 recorded tests (*.recorded.test.ts)
  vitest.config.ts
```

**Adding a new recorded smoke test:**

1. **Capture a fixture** — Run the real CLI command and save the output to `tests/smoke/fixtures/<server>/<tool>/<scenario>.txt`
2. **Write the test** — Create or extend a file in `tests/smoke/suite/`. Follow the existing pattern: mock the runner with `vi.mocked(git).mockResolvedValueOnce()`, call the tool handler, and validate `structuredContent` against the Zod schema
3. **Update the scenario file** — Change the scenario's status from `mocked` → `complete` in the corresponding `tests/smoke/scenarios/*.md` file
4. **Run `pnpm smoke`** — Verify all tests pass

**Key patterns to follow:**

- Always add `vi.mocked(runner).mockReset()` in `beforeEach` — `vi.clearAllMocks()` does not clear `mockResolvedValueOnce` queues
- Pass `compact: false` for tools using `compactDualOutput` to get full field objects in tests
- FakeServer doesn't apply Zod `.default()` values — pass all params with defaults explicitly
- Check the tool implementation for the exact number of `git()` calls to mock (some tools make multiple calls)

## Code Style

- **Prettier**: Formatting is handled automatically by **Husky** and **lint-staged** via a pre-commit hook. You don't need to format your code manually.
- **ESLint**: Run `pnpm lint` to check for code quality issues.

## Running Checks

```bash
pnpm build          # Build all packages
pnpm test           # Run all tests
pnpm smoke          # Run smoke tests
pnpm lint           # Lint all packages
pnpm format:check   # Check formatting
```

## Key Principles

1. **Always use `outputSchema` + `structuredContent`** — This is Pare's core differentiator.
2. **Always include human-readable `content`** — Fallback for clients without structuredContent support.
3. **Strip aggressively** — Only include data an agent would act on. No hints, no decorations.
4. **Use `execFile`, not `exec`** — Prevents shell injection.
5. **Test with real output** — Capture actual CLI output as test fixtures.

## Canary (Pre-release) Versions

Canary releases let you test unpublished changes from any branch without affecting the `latest` npm tag.

**How to publish a canary:**

1. Go to **Actions > Canary Release** in GitHub
2. Click **Run workflow** and select the branch you want to publish from
3. The workflow builds, tests, then publishes snapshot versions tagged as `canary`

The published versions look like `0.10.2-canary-20260223-abc1234` and are installed with:

```bash
npm install @paretools/git@canary
```

Canary versions never become `latest` -- they are published under the `canary` dist-tag only.

You can also generate canary version numbers locally (without publishing) using:

```bash
pnpm canary
```

## Commit Messages

Use conventional commits:

- `feat(git): add stash tool`
- `fix(shared): handle binary file in ANSI strip`
- `docs: update README with npm server example`
- `chore: update dependencies`
