SendKit: one messaging library, three ways to use it
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:
- CLI (Commander): reads the command-line arguments and calls the core.
- Local MCP server: uses stdio and exposes the core schemas as MCP tools.
- Remote MCP server (Hono): uses HTTP, same schemas, plus authorization.
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