Context
Charter's primitives are exposed through three surfaces: the programmatic API (@stackbilt/blast, @stackbilt/validate, etc.), the CLI (charter <cmd>), and the MCP server (charter serve). Today these surfaces drift:
- API contracts are TypeScript
interfaces — compile-time only, no runtime validation, no machine-readable schema.
- CLI commands parse argv and format output against those types manually.
charter serve exposes ADF-curated context as MCP resources but registers zero server.tool(...) calls — so agents can read governance but can't invoke primitives.
Result: adding a parameter to a core function means touching the API, the CLI adapter, any MCP tool wrapper, and the docs independently. The surfaces can silently disagree.
Proposal: Zod-Core-Out pattern
Make the Zod schema the single source of truth for each primitive's input/output. CLI and MCP become thin adapters that translate their respective input formats (argv, JSON-RPC) into the same schema, call the same pure core function, and format the same structured result for their respective consumers.
┌─────────────────────────┐
│ @stackbilt/blast │ Pure core + Zod schemas
│ - BlastInputSchema │ - z.describe() for LLMs
│ - BlastOutputSchema │ - z.infer<> → TS types
│ - blastRadius() │ - No I/O, no chalk, no prompts
└──────────┬──────────────┘
│
┌─────┴─────┐
▼ ▼
┌─────────┐ ┌─────────────┐
│ CLI │ │ MCP tool │
│ argv → │ │ JSON-RPC → │
│ schema │ │ schema │
│ → core │ │ → core │
│ → ANSI │ │ → JSON │
└─────────┘ └─────────────┘
Vertical slice: blast
Scope this PR to @stackbilt/blast end-to-end. Every other package follows the same template afterward.
Additive only (per OSS update policy):
- Add
BlastInputSchema and BlastOutputSchema to packages/blast/src/index.ts.
- Keep existing
BlastOptions / BlastRadiusResult interfaces — re-export as z.infer<> aliases so no consumer breaks.
- Add
.describe() strings on every schema field — this is the LLM-facing documentation that MCP clients surface.
- Register
charter_blast as an MCP tool in packages/cli/src/commands/serve.ts via server.tool(...) — it validates with BlastInputSchema.parse, calls blastRadius, returns structured JSON.
- Refactor
packages/cli/src/commands/blast.ts to parse argv → BlastInputSchema → core → formatter. Human output unchanged.
Acceptance criteria
Why this first
blast is the right proof-of-concept because:
- The package is already dependency-free and headless — minimal refactor cost.
- Blast radius is an obvious agent-use-case ("what breaks if I change this file?").
- Once the pattern is proven,
validate, drift, classify, and surface each become a ~1-day port.
Out of scope
- SSE transport for
charter serve (separate issue).
- MCP tool wrappers for other packages (follow-up issues once the pattern lands).
- Breaking changes to existing types (policy: additive only).
Context
Charter's primitives are exposed through three surfaces: the programmatic API (
@stackbilt/blast,@stackbilt/validate, etc.), the CLI (charter <cmd>), and the MCP server (charter serve). Today these surfaces drift:interfaces — compile-time only, no runtime validation, no machine-readable schema.charter serveexposes ADF-curated context as MCP resources but registers zeroserver.tool(...)calls — so agents can read governance but can't invoke primitives.Result: adding a parameter to a core function means touching the API, the CLI adapter, any MCP tool wrapper, and the docs independently. The surfaces can silently disagree.
Proposal: Zod-Core-Out pattern
Make the Zod schema the single source of truth for each primitive's input/output. CLI and MCP become thin adapters that translate their respective input formats (argv, JSON-RPC) into the same schema, call the same pure core function, and format the same structured result for their respective consumers.
Vertical slice: blast
Scope this PR to
@stackbilt/blastend-to-end. Every other package follows the same template afterward.Additive only (per OSS update policy):
BlastInputSchemaandBlastOutputSchematopackages/blast/src/index.ts.BlastOptions/BlastRadiusResultinterfaces — re-export asz.infer<>aliases so no consumer breaks..describe()strings on every schema field — this is the LLM-facing documentation that MCP clients surface.charter_blastas an MCP tool inpackages/cli/src/commands/serve.tsviaserver.tool(...)— it validates withBlastInputSchema.parse, callsblastRadius, returns structured JSON.packages/cli/src/commands/blast.tsto parse argv →BlastInputSchema→ core → formatter. Human output unchanged.Acceptance criteria
@stackbilt/blastexports Zod schemas alongside existing types;BlastRadiusResultremains a valid type import.charter blast <path>output is byte-identical to current behavior (snapshot test).charter serveregisterscharter_blastas an MCP tool callable from Claude Code; tool schema is generated from Zod viazod-to-json-schema.Why this first
blast is the right proof-of-concept because:
validate,drift,classify, andsurfaceeach become a ~1-day port.Out of scope
charter serve(separate issue).