← all projects

SendKit — one messaging core, three published surfaces

TypeScript · Bun · Hono · MCP · Zod · Clerk

The problem

The same messaging capability needed to be reachable three ways: from a terminal, from a local MCP client over stdio, and from a remote MCP client over HTTP. The obvious approach — three implementations sharing a README — guarantees drift. Within a release or two the CLI accepts a field the HTTP surface rejects, and the bug reports are impossible to reason about because there is no single source of truth.

The decision

One core package owns the messaging logic and its Zod schemas. The three surfaces are thin adapters that translate their transport into a core call:

Because the Zod schemas are the validation and are the MCP tool definitions, adding a field is one edit that propagates to all three surfaces. The contract cannot drift, structurally.

Remote authorization

The remote server implements Clerk OAuth 2.0 with RFC 9728 protected-resource metadata — the discovery mechanism that lets an MCP client find out where to get a token and what scope it needs, without out-of-band configuration. That's what makes the remote server usable by a client that has never seen it before.

Local stdio and remote HTTP therefore differ in exactly one dimension — authorization — rather than in their tool surface.

Release pipeline

tsdown for builds, oxlint and oxfmt for lint and formatting, scoped npm publishing for the packages. The pipeline was set up before the second surface existed, which is the right order: publishing friction is what causes people to stop factoring things properly.

Stack

TypeScript · Bun workspaces · Zod · Hono · Model Context Protocol · Clerk · Commander · tsdown · npm