@outfitter/presets 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/dist/index.js +1 -1
- package/package.json +25 -24
- package/presets/_base/AGENTS.md.template +3 -0
- package/presets/_base/CLAUDE.md.template +35 -0
- package/presets/_examples/cli-todo/src/program.ts.template +202 -0
- package/presets/_examples/mcp-files/src/mcp.ts.template +181 -0
- package/presets/basic/.lefthook.yml.template +2 -2
- package/presets/basic/README.md.template +22 -0
- package/presets/basic/package.json.template +41 -37
- package/presets/basic/src/index.test.ts.template +26 -0
- package/presets/basic/src/index.ts.template +32 -15
- package/presets/basic/tsconfig.json.template +28 -29
- package/presets/cli/.lefthook.yml.template +2 -2
- package/presets/cli/CLAUDE.md.template +60 -0
- package/presets/cli/README.md.template +5 -1
- package/presets/cli/package.json.template +47 -44
- package/presets/cli/src/commands/hello.ts.template +26 -0
- package/presets/cli/src/index.test.ts.template +38 -0
- package/presets/cli/src/index.ts.template +2 -0
- package/presets/cli/src/program.ts.template +17 -19
- package/presets/cli/src/types.ts.template +13 -0
- package/presets/cli/tsconfig.json.template +29 -29
- package/presets/daemon/.lefthook.yml.template +2 -2
- package/presets/daemon/CLAUDE.md.template +53 -0
- package/presets/daemon/README.md.template +6 -7
- package/presets/daemon/package.json.template +50 -47
- package/presets/daemon/src/cli.ts.template +73 -66
- package/presets/daemon/src/daemon-main.ts.template +56 -55
- package/presets/daemon/src/daemon.ts.template +7 -3
- package/presets/daemon/src/index.test.ts.template +9 -0
- package/presets/daemon/tsconfig.json.template +21 -21
- package/presets/full-stack/CLAUDE.md.template +66 -0
- package/presets/full-stack/apps/cli/package.json.template +34 -33
- package/presets/full-stack/apps/cli/src/cli.ts.template +16 -15
- package/presets/full-stack/apps/cli/src/index.test.ts.template +12 -11
- package/presets/full-stack/apps/cli/tsconfig.json.template +32 -32
- package/presets/full-stack/apps/mcp/package.json.template +35 -34
- package/presets/full-stack/apps/mcp/src/index.test.ts.template +12 -11
- package/presets/full-stack/apps/mcp/src/mcp.ts.template +10 -10
- package/presets/full-stack/apps/mcp/src/server.ts.template +3 -2
- package/presets/full-stack/apps/mcp/tsconfig.json.template +32 -32
- package/presets/full-stack/package.json.template +20 -14
- package/presets/full-stack/packages/core/package.json.template +31 -30
- package/presets/full-stack/packages/core/src/handlers.ts.template +29 -24
- package/presets/full-stack/packages/core/src/index.test.ts.template +23 -21
- package/presets/full-stack/packages/core/src/types.ts.template +11 -8
- package/presets/full-stack/packages/core/tsconfig.json.template +29 -29
- package/presets/library/CLAUDE.md.template +68 -0
- package/presets/library/bunup.config.ts.template +16 -16
- package/presets/library/package.json.template +51 -50
- package/presets/library/src/handlers.ts.template +51 -27
- package/presets/library/src/index.test.ts.template +40 -29
- package/presets/library/src/types.ts.template +14 -8
- package/presets/library/tsconfig.json.template +29 -29
- package/presets/mcp/.lefthook.yml.template +2 -2
- package/presets/mcp/CLAUDE.md.template +97 -0
- package/presets/mcp/README.md.template +12 -9
- package/presets/mcp/package.json.template +48 -44
- package/presets/mcp/src/index.test.ts.template +49 -0
- package/presets/mcp/src/index.ts.template +2 -0
- package/presets/mcp/src/mcp.ts.template +16 -16
- package/presets/mcp/src/server.ts.template +8 -1
- package/presets/mcp/src/tools/hello.ts.template +48 -0
- package/presets/mcp/tsconfig.json.template +21 -21
- package/presets/minimal/.lefthook.yml.template +2 -2
- package/presets/minimal/README.md.template +22 -0
- package/presets/minimal/package.json.template +47 -44
- package/presets/minimal/src/index.test.ts.template +19 -0
- package/presets/minimal/src/index.ts.template +10 -15
- package/presets/minimal/tsconfig.json.template +28 -29
- package/presets/cli/biome.json.template +0 -4
- package/presets/daemon/biome.json.template +0 -4
- package/presets/mcp/biome.json.template +0 -4
|
@@ -1,34 +1,34 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "ESNext",
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"lib": ["ESNext"],
|
|
8
|
+
"types": ["@types/bun"],
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
"strict": true,
|
|
11
|
+
"noImplicitAny": true,
|
|
12
|
+
"strictNullChecks": true,
|
|
13
|
+
"noUncheckedIndexedAccess": true,
|
|
14
|
+
"exactOptionalPropertyTypes": true,
|
|
15
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
16
|
+
"noImplicitReturns": true,
|
|
17
|
+
"noFallthroughCasesInSwitch": true,
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
"declaration": true,
|
|
20
|
+
"declarationMap": true,
|
|
21
|
+
"sourceMap": true,
|
|
22
|
+
"outDir": "./dist",
|
|
23
|
+
"rootDir": "./src",
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
"esModuleInterop": true,
|
|
26
|
+
"forceConsistentCasingInFileNames": true,
|
|
27
|
+
"isolatedModules": true,
|
|
28
|
+
"verbatimModuleSyntax": true,
|
|
29
|
+
"skipLibCheck": true,
|
|
30
|
+
"resolveJsonModule": true
|
|
31
|
+
},
|
|
32
|
+
"include": ["src/**/*"],
|
|
33
|
+
"exclude": ["node_modules", "dist"]
|
|
34
34
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Bun-first TypeScript library. Tests before code. Result types, not exceptions.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun run build # Build library with bunup (ESM + types)
|
|
9
|
+
bun run dev # Watch mode
|
|
10
|
+
bun run test # Run tests
|
|
11
|
+
bun run typecheck # TypeScript validation
|
|
12
|
+
bun run check # Lint + format check (ultracite)
|
|
13
|
+
bun run lint # Lint checks (oxlint)
|
|
14
|
+
bun run lint:fix # Auto-fix lint issues
|
|
15
|
+
bun run format # Auto-fix formatting (oxfmt)
|
|
16
|
+
bun run verify:ci # Full CI validation (typecheck + check + build + test)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Architecture
|
|
20
|
+
|
|
21
|
+
Publishable TypeScript library built with Bun and bunup.
|
|
22
|
+
|
|
23
|
+
### Project Structure
|
|
24
|
+
|
|
25
|
+
- `src/types.ts` — Zod schemas with explicit `ZodType` annotations + TypeScript interfaces
|
|
26
|
+
- `src/handlers.ts` — Pure handler functions returning `Result<T, E>`
|
|
27
|
+
- `src/index.ts` — Re-exports from types + handlers
|
|
28
|
+
- `src/index.test.ts` — Tests with `createContext()`, testing success + error paths
|
|
29
|
+
|
|
30
|
+
### Handler Contract
|
|
31
|
+
|
|
32
|
+
All domain logic uses transport-agnostic handlers returning `Result<T, E>`:
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
async function handler(
|
|
36
|
+
input: unknown,
|
|
37
|
+
ctx: HandlerContext
|
|
38
|
+
): Promise<Result<Output, ValidationError>> {
|
|
39
|
+
const parsed = inputSchema.safeParse(input);
|
|
40
|
+
if (!parsed.success) {
|
|
41
|
+
return Result.err(new ValidationError({ message: "...", field: "name" }));
|
|
42
|
+
}
|
|
43
|
+
// business logic
|
|
44
|
+
return Result.ok(result);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Schema Convention
|
|
49
|
+
|
|
50
|
+
Exported Zod schemas must have explicit type annotations:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
export const inputSchema: ZodType<Input> = z.object({ ... });
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Development Principles
|
|
57
|
+
|
|
58
|
+
- **TDD-First** — Write the test before the code (Red / Green / Refactor)
|
|
59
|
+
- **Result Types** — Handlers return `Result<T, E>`, not exceptions
|
|
60
|
+
- **Bun-First** — Use Bun-native APIs before npm packages
|
|
61
|
+
- **Strict TypeScript** — No `any`, no `as` casts; narrow instead of assert
|
|
62
|
+
|
|
63
|
+
## Testing
|
|
64
|
+
|
|
65
|
+
- Runner: Bun test runner
|
|
66
|
+
- Files: `src/*.test.ts`
|
|
67
|
+
- Run: `bun test` or `bun run test`
|
|
68
|
+
- Test handlers with `createContext()` from `@outfitter/contracts`
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { defineWorkspace } from "bunup";
|
|
2
2
|
|
|
3
3
|
export default defineWorkspace(
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
4
|
+
[
|
|
5
|
+
{
|
|
6
|
+
name: "{{packageName}}",
|
|
7
|
+
root: ".",
|
|
8
|
+
},
|
|
9
|
+
],
|
|
10
|
+
{
|
|
11
|
+
entry: ["src/**/*.ts", "!src/**/*.test.ts", "!src/**/__tests__/**"],
|
|
12
|
+
sourceBase: "./src",
|
|
13
|
+
format: ["esm", "cjs"],
|
|
14
|
+
dts: true,
|
|
15
|
+
exports: true,
|
|
16
|
+
splitting: true,
|
|
17
|
+
clean: true,
|
|
18
|
+
target: "bun",
|
|
19
|
+
}
|
|
20
20
|
);
|
|
@@ -1,52 +1,53 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
2
|
+
"name": "{{packageName}}",
|
|
3
|
+
"version": "{{version}}",
|
|
4
|
+
"description": "{{description}}",
|
|
5
|
+
"keywords": [],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"type": "module",
|
|
11
|
+
"sideEffects": false,
|
|
12
|
+
"main": "./dist/index.cjs",
|
|
13
|
+
"module": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js",
|
|
19
|
+
"require": "./dist/index.cjs"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "bunup",
|
|
24
|
+
"dev": "bun --watch src/index.ts",
|
|
25
|
+
"test": "bun test",
|
|
26
|
+
"test:watch": "bun test --watch",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"check": "ultracite check",
|
|
29
|
+
"lint": "oxlint .",
|
|
30
|
+
"lint:fix": "oxlint --fix .",
|
|
31
|
+
"format": "oxfmt --write .",
|
|
32
|
+
"verify:ci": "bun run typecheck && bun run check && bun run build && bun run test",
|
|
33
|
+
"clean:artifacts": "rm -rf dist .turbo",
|
|
34
|
+
"clean": "rm -rf dist"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@outfitter/contracts": "workspace:*",
|
|
38
|
+
"zod": "catalog:"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"bunup": "catalog:"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"bun": ">=1.3.10"
|
|
45
|
+
},
|
|
46
|
+
"outfitter": {
|
|
47
|
+
"template": {
|
|
48
|
+
"kind": "library",
|
|
49
|
+
"placement": "packages",
|
|
50
|
+
"surfaces": []
|
|
51
|
+
}
|
|
52
|
+
}
|
|
52
53
|
}
|
|
@@ -1,31 +1,55 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
Result,
|
|
3
|
+
ValidationError,
|
|
4
|
+
type HandlerContext,
|
|
5
|
+
} from "@outfitter/contracts";
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
import { greetingInputSchema, type Greeting } from "./types.js";
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Create a greeting from unvalidated input.
|
|
11
|
+
*
|
|
12
|
+
* @param input - Raw input to validate against {@link greetingInputSchema}
|
|
13
|
+
* @param ctx - Handler context with logger and request metadata
|
|
14
|
+
* @returns Validated greeting or a `ValidationError` with field-level validation details
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const result = await createGreeting({ name: "World" }, ctx);
|
|
19
|
+
* if (result.isOk()) {
|
|
20
|
+
* console.log(result.value.message); // "Hello, World."
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export async function createGreeting(
|
|
25
|
+
input: unknown,
|
|
26
|
+
ctx: HandlerContext
|
|
27
|
+
): Promise<Result<Greeting, ValidationError>> {
|
|
28
|
+
const parsed = greetingInputSchema.safeParse(input);
|
|
29
|
+
if (!parsed.success) {
|
|
30
|
+
const issue = parsed.error.issues[0];
|
|
31
|
+
return Result.err(
|
|
32
|
+
new ValidationError({
|
|
33
|
+
message: issue?.message ?? "Invalid greeting input",
|
|
34
|
+
field: issue?.path.join(".") || "input",
|
|
35
|
+
context: {
|
|
36
|
+
fields: parsed.error.issues.map((i) => ({
|
|
37
|
+
path: i.path.join(".") || "input",
|
|
38
|
+
message: i.message,
|
|
39
|
+
})),
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
}
|
|
20
44
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
45
|
+
const suffix = parsed.data.excited ? "!" : ".";
|
|
46
|
+
const greeting: Greeting = {
|
|
47
|
+
message: `Hello, ${parsed.data.name}${suffix}`,
|
|
48
|
+
issuedAt: new Date().toISOString(),
|
|
49
|
+
};
|
|
26
50
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
51
|
+
ctx.logger.info(`Created greeting for ${parsed.data.name}`, {
|
|
52
|
+
requestId: ctx.requestId,
|
|
53
|
+
});
|
|
54
|
+
return Result.ok(greeting);
|
|
55
|
+
}
|
|
@@ -1,35 +1,46 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
2
3
|
import { createContext } from "@outfitter/contracts";
|
|
4
|
+
|
|
3
5
|
import { createGreeting } from "./handlers.js";
|
|
4
6
|
|
|
5
7
|
describe("createGreeting", () => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
8
|
+
const ctx = createContext({ cwd: process.cwd(), env: process.env });
|
|
9
|
+
|
|
10
|
+
test("returns greeting for valid input", async () => {
|
|
11
|
+
const result = await createGreeting(
|
|
12
|
+
{ name: "Outfitter", excited: true },
|
|
13
|
+
ctx
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
expect(result.isOk()).toBe(true);
|
|
17
|
+
if (result.isErr()) return;
|
|
18
|
+
expect(result.value.message).toBe("Hello, Outfitter!");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("returns validation error for empty name", async () => {
|
|
22
|
+
const result = await createGreeting({ name: "" }, ctx);
|
|
23
|
+
|
|
24
|
+
expect(result.isErr()).toBe(true);
|
|
25
|
+
if (!result.isErr()) return;
|
|
26
|
+
expect(result.error.name).toBe("ValidationError");
|
|
27
|
+
expect(result.error.message).toBe("name is required");
|
|
28
|
+
expect(result.error.context).toEqual({
|
|
29
|
+
fields: [{ path: "name", message: "name is required" }],
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("returns validation error for missing input", async () => {
|
|
34
|
+
const result = await createGreeting({}, ctx);
|
|
35
|
+
|
|
36
|
+
expect(result.isErr()).toBe(true);
|
|
37
|
+
if (!result.isErr()) return;
|
|
38
|
+
expect(result.error.name).toBe("ValidationError");
|
|
39
|
+
// Check error field rather than message text — Zod error messages
|
|
40
|
+
// differ between v3 ("Required") and v4 ("Invalid input: ...").
|
|
41
|
+
const fields = result.error.context?.fields as
|
|
42
|
+
| Array<{ path: string }>
|
|
43
|
+
| undefined;
|
|
44
|
+
expect(fields?.some((f) => f.path === "name")).toBe(true);
|
|
45
|
+
});
|
|
35
46
|
});
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
1
|
+
import { type ZodType, z } from "zod";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
/** Input for the greeting handler. */
|
|
4
|
+
export interface GreetingInput {
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly excited: boolean;
|
|
7
|
+
}
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
/** Zod schema for validating greeting input at the boundary. */
|
|
10
|
+
export const greetingInputSchema: ZodType<GreetingInput> = z.object({
|
|
11
|
+
name: z.string().min(1, "name is required"),
|
|
12
|
+
excited: z.boolean().default(false),
|
|
13
|
+
});
|
|
9
14
|
|
|
15
|
+
/** A generated greeting with timestamp. */
|
|
10
16
|
export interface Greeting {
|
|
11
|
-
|
|
12
|
-
|
|
17
|
+
readonly message: string;
|
|
18
|
+
readonly issuedAt: string;
|
|
13
19
|
}
|
|
@@ -1,34 +1,34 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "ESNext",
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"lib": ["ESNext"],
|
|
8
|
+
"types": ["@types/bun"],
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
"strict": true,
|
|
11
|
+
"noImplicitAny": true,
|
|
12
|
+
"strictNullChecks": true,
|
|
13
|
+
"noUncheckedIndexedAccess": true,
|
|
14
|
+
"exactOptionalPropertyTypes": true,
|
|
15
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
16
|
+
"noImplicitReturns": true,
|
|
17
|
+
"noFallthroughCasesInSwitch": true,
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
"declaration": true,
|
|
20
|
+
"declarationMap": true,
|
|
21
|
+
"sourceMap": true,
|
|
22
|
+
"outDir": "./dist",
|
|
23
|
+
"rootDir": "./src",
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
"esModuleInterop": true,
|
|
26
|
+
"forceConsistentCasingInFileNames": true,
|
|
27
|
+
"isolatedModules": true,
|
|
28
|
+
"verbatimModuleSyntax": true,
|
|
29
|
+
"skipLibCheck": true,
|
|
30
|
+
"resolveJsonModule": true
|
|
31
|
+
},
|
|
32
|
+
"include": ["src/**/*"],
|
|
33
|
+
"exclude": ["node_modules", "dist"]
|
|
34
34
|
}
|
|
@@ -6,12 +6,12 @@ pre-commit:
|
|
|
6
6
|
commands:
|
|
7
7
|
format:
|
|
8
8
|
glob: "*.{js,ts,tsx,json,md}"
|
|
9
|
-
run: bunx
|
|
9
|
+
run: bunx oxfmt --write {staged_files}
|
|
10
10
|
stage_fixed: true
|
|
11
11
|
|
|
12
12
|
lint:
|
|
13
13
|
glob: "*.{js,ts,tsx}"
|
|
14
|
-
run: bunx
|
|
14
|
+
run: bunx oxlint {staged_files}
|
|
15
15
|
|
|
16
16
|
typecheck:
|
|
17
17
|
glob: "*.{ts,tsx}"
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Bun-first TypeScript MCP server. Tests before code. Result types, not exceptions.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun run build # Build server + library to dist/
|
|
9
|
+
bun run dev # Watch mode (auto-restart on changes)
|
|
10
|
+
bun run test # Run tests
|
|
11
|
+
bun run typecheck # TypeScript validation
|
|
12
|
+
bun run check # Lint + format check
|
|
13
|
+
bun run lint:fix # Auto-fix lint issues
|
|
14
|
+
bun run format # Auto-fix formatting
|
|
15
|
+
bun run verify:ci # Full CI validation (typecheck + check + build + test)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Architecture
|
|
19
|
+
|
|
20
|
+
MCP (Model Context Protocol) server built with `@modelcontextprotocol/sdk` and `@outfitter/mcp`.
|
|
21
|
+
|
|
22
|
+
### Project Structure
|
|
23
|
+
|
|
24
|
+
- `src/server.ts` — Server entry point (stdio transport)
|
|
25
|
+
- `src/mcp.ts` — Tool and resource registration
|
|
26
|
+
- `src/tools/` — Tool definitions with Zod schemas and handler logic
|
|
27
|
+
- `src/index.ts` — Library re-exports for testing
|
|
28
|
+
|
|
29
|
+
### Tool Definition
|
|
30
|
+
|
|
31
|
+
Tools are defined with `defineTool()` using Zod schemas for input validation and Result types for output:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { Result, ValidationError } from "@outfitter/contracts";
|
|
35
|
+
import { defineTool } from "@outfitter/mcp";
|
|
36
|
+
import { z } from "zod";
|
|
37
|
+
|
|
38
|
+
const myTool = defineTool({
|
|
39
|
+
name: "my-tool",
|
|
40
|
+
description: "Does something useful",
|
|
41
|
+
inputSchema: z.object({ query: z.string().min(1) }),
|
|
42
|
+
annotations: { readOnlyHint: true },
|
|
43
|
+
handler: async (input, ctx): Promise<Result<MyOutput, ValidationError>> => {
|
|
44
|
+
if (!isValid(input.query)) {
|
|
45
|
+
return Result.err(ValidationError.create("query", "invalid format"));
|
|
46
|
+
}
|
|
47
|
+
return Result.ok({ data: await fetchData(input.query) });
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Resource Definition
|
|
53
|
+
|
|
54
|
+
Static resources use `defineResource()`:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { Result } from "@outfitter/contracts";
|
|
58
|
+
import { defineResource } from "@outfitter/mcp";
|
|
59
|
+
|
|
60
|
+
const myResource = defineResource({
|
|
61
|
+
uri: "myapp:///config",
|
|
62
|
+
name: "Config",
|
|
63
|
+
handler: async (uri, _ctx) =>
|
|
64
|
+
Result.ok([{ uri, text: JSON.stringify(config) }]),
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Adding a Resource
|
|
69
|
+
|
|
70
|
+
Simple, self-contained resources (like `versionResource` in `src/mcp.ts`) can be defined inline — no separate file or re-export needed. For resources with non-trivial logic, extract to a dedicated file:
|
|
71
|
+
|
|
72
|
+
1. Create resource file in `src/resources/<name>.ts` with `defineResource()`
|
|
73
|
+
2. Import and register in `src/mcp.ts` via `server.registerResource()`
|
|
74
|
+
3. Re-export from `src/index.ts`
|
|
75
|
+
4. Add tests in `src/<name>.test.ts`
|
|
76
|
+
|
|
77
|
+
### Adding a Tool
|
|
78
|
+
|
|
79
|
+
1. Create tool file in `src/tools/<name>.ts` with `defineTool()`
|
|
80
|
+
2. Import and register in `src/mcp.ts`
|
|
81
|
+
3. Re-export from `src/index.ts`
|
|
82
|
+
4. Add tests in `src/<name>.test.ts`
|
|
83
|
+
|
|
84
|
+
## Development Principles
|
|
85
|
+
|
|
86
|
+
- **TDD-First** — Write the test before the code (Red / Green / Refactor)
|
|
87
|
+
- **Result Types** — Handlers return `Result<T, E>`, not exceptions
|
|
88
|
+
- **Bun-First** — Use Bun-native APIs before npm packages
|
|
89
|
+
- **Strict TypeScript** — No `any`, no `as` casts; narrow instead of assert
|
|
90
|
+
|
|
91
|
+
## Testing
|
|
92
|
+
|
|
93
|
+
- Runner: Bun test runner
|
|
94
|
+
- Harness: `createMcpHarness()` from `@outfitter/testing` for protocol-level tests
|
|
95
|
+
- Files: `src/*.test.ts`
|
|
96
|
+
- Run: `bun test` or `bun run test`
|
|
97
|
+
- Test tool handlers directly by importing from `src/index.ts`
|