← all projects

SendKit: one messaging library, three ways to use it

TypeScript · Bun · Hono · MCP · Zod · Clerk

The problem

I wanted the same messaging feature to be available in three ways: from the terminal, from a local MCP client over stdio, and from a remote MCP client over HTTP. The easy approach is to write it three times. But then the three versions slowly get out of sync. After a release or two, the CLI accepts a field that the HTTP server rejects, and bugs become hard to track down because there is no single source of truth.

The decision

One core package holds the messaging logic and its Zod schemas. The three entry points are thin wrappers that turn their input into a call to the core:

The Zod schemas are used both for validation and as the MCP tool definitions. So adding a field is one change, and all three entry points get it. They cannot get out of sync.

Remote authorization

The remote server uses Clerk OAuth 2.0 with RFC 9728 protected resource metadata. This lets an MCP client find out where to get a token and which scope it needs, without any manual setup. Because of this, a client that has never seen the server before can still use it.

So the local and remote servers differ in only one thing, authorization. The tools are the same.

Release pipeline

tsdown for builds, oxlint and oxfmt for linting and formatting, and scoped npm packages for publishing. I set up the pipeline before building the second entry point. That was the right order, because when publishing is painful, people stop splitting code into clean packages.

Stack

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