Build agent-ready tools with one shared TypeScript core for MCP, CLI, and Skills.
Prompt: "Send Antonio a Telegram message saying the build shipped."
Agent: Uses SendKit's
telegramMCP tool with{ "chatId": "...", "message": "The build shipped." }, backed by the same core operation available from the CLI and Skill.
npm install -g @cwa-dev/sendkit
sendkit init --telegram-bot-token "<bot-token>"
sendkit telegram "<chat-id>" "Hello from SendKit"Table of contents
This repository is built from scratch, step by step, in a 4-hour tutorial:
The Only Guide You Need to Build Claude Skills and MCP Servers ▶
It walks through the entire core -> CLI -> MCP -> Skill pattern, including the local and remote MCP adapters and Clerk-protected remote deployment.
SendKit is both a tutorial and a boilerplate for modern agent tooling. It shows how one shared operation can become a complete agent-facing toolkit: a CLI command, a local MCP tool, a remote MCP server, and Skill instructions.
The example operation sends Telegram messages, but the structure is intentionally easy to replace. Swap the operation in packages/core, update the adapters, and you have a strong starting point for a different product, internal tool, workflow automation, or agent capability.
The central pattern is:
flowchart LR
A[Core capability] --> B[CLI command]
A --> C[MCP tool]
A --> D[Skill instructions]
Business logic lives in packages/core. Every other package is an adapter, so agents, scripts, and humans all use the same implementation instead of drifting copies.
Start with the section that matches your goal:
- Use SendKit: Install the published CLI, local MCP server, and Skill.
- Fork Or Adapt SendKit: Turn the boilerplate into your own agent tooling project.
- Run Locally: Develop the tutorial or modify the packages from source.
- Publish Packages To NPM: Release notes for maintainers.
| Package | Role |
|---|---|
packages/core |
Shared schemas and operations. |
packages/cli |
Human/script CLI adapter. |
packages/local-mcp |
Local MCP stdio server adapter for AI clients. |
apps/remote-mcp |
Remote MCP HTTP adapter for deployed clients. |
skills/sendkit |
Agent-facing usage instructions. |
- A Telegram bot token.
- Node.js for published package usage.
- Bun if you want to run or adapt this repository locally.
- A Node-compatible MCP client if you want to connect the local MCP server.
Use this path when you want the published SendKit tools, not the source workspace.
Install the CLI globally:
npm install -g @cwa-dev/sendkitConfigure your Telegram bot token:
sendkit init --telegram-bot-token "<bot-token>"Send a message:
sendkit telegram "<chat-id>" "Hello from SendKit"Use JSON output when scripting or when an agent needs to parse the result:
sendkit telegram "<chat-id>" "Hello from SendKit" --jsonExpected JSON output:
{
"ok": true,
"chatId": "<chat-id>",
"messageId": 123
}CLI config is stored at ~/.config/sendkit/config.json.
Install the local MCP stdio server globally:
npm install -g @cwa-dev/sendkit-mcpConfigure your MCP client to run sendkit-mcp and pass TELEGRAM_BOT_TOKEN through the server environment:
{
"mcpServers": {
"sendkit": {
"command": "sendkit-mcp",
"args": [],
"environment": {
"TELEGRAM_BOT_TOKEN": "<bot-token>"
}
}
}
}If your MCP client can execute npm packages directly, skip the global install:
{
"mcpServers": {
"sendkit": {
"command": "npx",
"args": ["-y", "@cwa-dev/sendkit-mcp"],
"environment": {
"TELEGRAM_BOT_TOKEN": "<bot-token>"
}
}
}
}Available MCP tools:
telegram: Accepts{ chatId, message }and returns{ ok, chatId, messageId }.
Do not include Telegram bot tokens in MCP tool arguments. The local MCP server reads the token from TELEGRAM_BOT_TOKEN.
Install the SendKit Skill with your skill manager:
npx skills add https://github.com/code-with-antonio/sendkit/tree/main/skills/sendkitThe Skill tells agents when to use the MCP telegram tool, when to fall back to the CLI, why --json matters for parsing, and why @cwa-dev/sendkit-core is only an implementation detail.
CLI fallback example from the Skill:
sendkit init --telegram-bot-token "<bot-token>"
sendkit telegram "<chat-id>" "Hello from SendKit" --jsonThis repository includes a remote MCP HTTP server, but SendKit does not provide a hosted public endpoint.
For remote MCP usage, deploy your own copy of apps/remote-mcp. The server exposes POST /:botToken/mcp, where botToken is the URL-encoded Telegram bot token for that request.
The remote MCP app can be protected with Clerk OAuth while keeping the Telegram bot token in the MCP URL:
https://your-sendkit-host.example.com/<telegram-bot-token>/mcp
Treat this URL like a secret. The URL still contains the Telegram bot token. If it is exposed, revoke and rotate the token with BotFather.
Set Clerk environment variables before starting the remote MCP app:
CLERK_PUBLISHABLE_KEY="<publishable-key>" \
CLERK_SECRET_KEY="<secret-key>" \
bun run dev:remote-mcpFor MCP OAuth clients, unauthenticated requests receive 401 Unauthorized with WWW-Authenticate metadata. The MCP client is responsible for opening the Clerk login flow.
In the Clerk Dashboard, enable Dynamic client registration for OAuth applications before testing with MCP clients that require automatic OAuth client registration.
This tutorial step demonstrates OAuth-protected MCP access. Clerk does not manage the Telegram bot token in this example; the token still comes from the URL.
ChatGPT web and Claude web support can vary by current MCP OAuth client behavior.
Claude web connects out of the box: it performs OAuth Dynamic Client Registration automatically, so no manual OAuth client setup is needed.
ChatGPT connectors and custom apps do not support Dynamic Client Registration. You must create your own OAuth client in the Clerk Dashboard, then:
- Copy ChatGPT's redirect/callback URI and add it to the allowed redirect URIs on your Clerk OAuth client.
- Provide the resulting client ID and client secret to ChatGPT when configuring the connector.
ChatGPT connector configuration:
Name: SendKit
Description: Send Telegram messages through SendKit MCP.
MCP Server URL: https://your-sendkit-host.example.com/<telegram-bot-token>/mcp
Authentication: OAuth
Example OpenCode remote MCP config after you deploy your own server:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"sendkit": {
"type": "remote",
"url": "https://your-host.example.com/{env:TELEGRAM_BOT_TOKEN}/mcp",
"enabled": true
}
}
}This keeps Telegram bot tokens user-specific and client-provided without exposing them as MCP tool arguments.
Use this path when SendKit is a starting point for something else. The Telegram operation is deliberately small so you can replace it with your own API wrapper, internal workflow, SaaS integration, developer tool, or automation.
Keep the dependency direction:
packages/core -> shared schemas and operations
packages/cli -> command-line adapter backed by core
packages/local-mcp -> local MCP stdio adapter backed by core
apps/remote-mcp -> remote MCP HTTP adapter backed by core
packages/skills/... -> agent-facing instructions and fallback guidance
Keep business logic in packages/core. Adapters should only parse inputs, read credentials from the right place, call core, and format results.
CLI command = parse input + call core + print output
MCP tool = validate input + call core + return structured result
Remote MCP = read request auth + validate input + call core
Skill = instructions for when/how to use CLI or MCP
Most forks replace these SendKit-specific pieces:
- Package names in
package.jsonfiles. - Binary names such as
sendkitandsendkit-mcp. - The Telegram operation in
packages/core/src/schemas.tsandpackages/core/src/operations.ts. - CLI commands in
packages/cli/src/index.ts. - MCP tool registrations in
packages/local-mcp/src/index.tsandapps/remote-mcp/src/index.ts. - Skill metadata and instructions in
skills/sendkit/SKILL.md. - README install, configuration, and verification commands.
Follow the explicit registration flow:
- Add input and output schemas in
packages/core/src/schemas.ts. - Add the operation function in
packages/core/src/operations.ts. - Export it through
packages/core/src/index.tswhen needed. - Add a CLI command in
packages/cli/src/index.ts. - Add a local MCP tool in
packages/local-mcp/src/index.ts. - Add a remote MCP tool in
apps/remote-mcp/src/index.tswhen remote support is part of your project. - Add usage notes in your Skill
SKILL.md. - Add manual verification commands to the README.
SendKit intentionally keeps registration explicit. Do not introduce a shared operation registry until the adapter boundaries are clear.
SendKit uses three credential paths because each interface has a different audience:
- CLI reads a persisted local user config created by
sendkit init. - Local MCP reads environment variables provided by the MCP client.
- Remote MCP reads the per-request Telegram bot token from the MCP URL path.
For your own project, keep credentials out of MCP tool input schemas unless the credential is genuinely part of the operation payload.
Use this path when you are following the tutorial, maintaining the repo, or changing packages before publishing.
Install workspace dependencies:
bun installRun the CLI from source:
bun run dev:cli init --telegram-bot-token "<bot-token>"
bun run dev:cli telegram "<chat-id>" "Hello from SendKit"
bun run dev:cli telegram "<chat-id>" "Hello from SendKit" --jsonStart the local MCP stdio server from source:
TELEGRAM_BOT_TOKEN="<bot-token>" bun run dev:local-mcpExpected behavior: the process stays open and waits for MCP messages over stdio. Stop it with Ctrl-C when testing manually.
Example MCP client config from the repository root:
{
"mcpServers": {
"sendkit": {
"command": "bun",
"args": ["run", "packages/local-mcp/src/index.ts"],
"environment": {
"TELEGRAM_BOT_TOKEN": "<bot-token>"
}
}
}
}Start the remote MCP HTTP server from source:
CLERK_PUBLISHABLE_KEY="<publishable-key>" \
CLERK_SECRET_KEY="<secret-key>" \
bun run dev:remote-mcpExpected behavior: the server listens on PORT or 3000 by default, exposes public protected resource metadata at GET /.well-known/oauth-protected-resource/:botToken/mcp, and protects POST /:botToken/mcp with Clerk OAuth.
Use bun link to test the CLI as a real sendkit command before publishing:
cd packages/cli
bun link
sendkit --helpAfter linking the binary, these can be run from anywhere on the machine:
sendkit init --telegram-bot-token "<bot-token>"
sendkit telegram "<chat-id>" "Hello from SendKit"
sendkit telegram "<chat-id>" "Hello from SendKit" --jsonTo remove the local linked binary, run this from packages/cli:
bun unlinkSee specs/CLI_LOCAL_LINK.md for the local linking acceptance criteria.
Run these before reporting implementation work complete:
bun install
bun run format
bun run lint
bun run typecheck
bun run dev:cli init --telegram-bot-token "<bot-token>"
bun run dev:cli telegram "<chat-id>" "Hello from SendKit"
bun run dev:cli telegram "<chat-id>" "Hello from SendKit" --json
TELEGRAM_BOT_TOKEN="<bot-token>" bun run dev:local-mcp
CLERK_PUBLISHABLE_KEY="<publishable-key>" CLERK_SECRET_KEY="<secret-key>" bun run dev:remote-mcpManual remote verification should confirm the server fails clearly without Clerk env vars, the metadata route returns public Clerk protected resource metadata, missing or invalid Authorization returns 401 with WWW-Authenticate, valid Clerk OAuth reaches MCP initialization, and the remote telegram MCP tool calls @cwa-dev/sendkit-core without exposing botToken in the tool input schema.
Dependency direction:
flowchart TD
CLI["@cwa-dev/sendkit<br/>(CLI)"] --> CORE["@cwa-dev/sendkit-core"]
MCP["@cwa-dev/sendkit-mcp<br/>(local MCP)"] --> CORE
REMOTE["sendkit-remote-mcp<br/>(remote MCP)"] --> CORE
SKILL["sendkit-skill<br/>(docs / instructions only)"] -.-> CORE
packages/core owns reusable logic:
- Zod schemas for shared inputs and outputs.
- Operation functions such as
sendTelegramMessage. - Type exports derived from schemas.
- No CLI imports, MCP SDK imports, terminal output, prompts, or
process.exit.
packages/cli owns human and script usage:
- Defines
sendkit telegram <chatId> <message>. - Parses command arguments with Commander.
- Calls
@cwa-dev/sendkit-corefunctions. - Prints readable output by default.
- Supports
--jsonfor scriptable and agent-readable output.
packages/local-mcp owns local MCP stdio usage:
- Creates an MCP stdio server.
- Registers a
telegramtool backed by@cwa-dev/sendkit-core. - Uses the shared Telegram message input schema.
- Returns both
contentandstructuredContent.
apps/remote-mcp owns remote MCP HTTP usage:
- Creates a Hono HTTP app exposing
/:botToken/mcp, run by Bun in development. - Registers a
telegramtool backed by@cwa-dev/sendkit-core. - Reads the Telegram bot token from the URL path per request.
- Keeps the token out of the MCP tool input schema.
- Closes the per-request MCP server after handling the request.
skills/sendkit owns agent instructions:
- Prefers the MCP
telegramtool when available. - Documents CLI fallback usage.
- Explains that
@cwa-dev/sendkit-coreis an implementation detail. - Avoids duplicating business logic.
SendKit includes one canonical tutorial operation: sendTelegramMessage.
Public interface names:
core function: sendTelegramMessage
CLI command: sendkit telegram <chatId> <message>
MCP tool: telegram
Skill usage: telegram
Telegram messages are sent through the Telegram Bot API. The CLI reads the bot token from local user config created by sendkit init. The local MCP server reads TELEGRAM_BOT_TOKEN from the MCP client-provided server environment. The remote MCP server reads the token from the per-request MCP URL path. All adapters pass the token into @cwa-dev/sendkit-core; it is not exposed as an MCP tool argument.
This section is for SendKit maintainers and fork authors publishing adapted packages. Normal users can skip it.
@cwa-dev/sendkit-core is the shared implementation package used by the CLI, MCP servers, and downstream programmatic consumers. Publish it from packages/core, not from the repository root.
@cwa-dev/sendkit is the published CLI package. It depends on the published core package, so publish core first whenever a release includes core changes.
@cwa-dev/sendkit-mcp is the published local MCP stdio server package. It also depends on the published core package, so publish core first whenever a release includes core changes.
npm versions are immutable. Before publishing, choose versions that have not already been published.
If core changed, bump packages/core/package.json first:
{
"name": "@cwa-dev/sendkit-core",
"version": "0.1.4"
}Leave the CLI and local MCP dependency on core as workspace:*. bun publish resolves workspace:* to the bumped core version at publish time, so you never hand-edit dependency ranges or risk shipping a workspace:* range to npm.
If the CLI changed, bump packages/cli/package.json and keep the CLI's displayed version in packages/cli/src/index.ts in sync with it.
If the local MCP server changed, bump packages/local-mcp/package.json and keep the MCP server version in packages/local-mcp/src/index.ts in sync with it.
After editing versions, refresh the lockfile from the repository root:
bun installRun the full workspace checks before publishing any package:
bun run release:checkThis script runs formatting checks, linting, typechecking, and all publishable package builds. Use bun run build:core, bun run build:cli, or bun run build:local-mcp when checking only one package build.
Package builds run through tsdown, which produces native Node ESM output in dist for publishing while keeping source imports clean. tsdown requires Node.js 22.18.0 or newer at build time, but the emitted package output targets the supported Node runtime.
Commit release preparation changes before running bun publish. The commit should include version bumps, lockfile updates, and documentation changes. Publishing from a committed state makes the npm package traceable to a specific repository revision and avoids publishing local edits that are not recorded in git.
After the publish succeeds, verify npm metadata and consider creating a git tag for the published version. If publishing fails before the package is accepted by npm, fix the issue, rerun the checks, and commit the fix before trying again. If npm accepts the publish but a later verification step fails, do not reuse the same version; bump to a new version for the next publish because npm versions are immutable.
Run the full publish workflow:
cd packages/core
bun publish --dry-run
bun publishbun publish runs the prepublishOnly build and reads publishConfig.access from package.json, so no separate build or --access flag is needed.
The dry run should include README.md, package.json, and compiled files under dist/, including:
dist/index.js
dist/index.d.ts
dist/index.js.map
The dry run should not include src/, node_modules/, or tsconfig.build.json.
If npm asks for a one-time password, rerun only the publish command with the current authenticator code:
bun publish --otp <code>If npm says you are not logged in, authenticate first. bun publish reuses the npm auth token from ~/.npmrc:
npm loginAfter publishing, verify the package metadata:
npm view @cwa-dev/sendkit-core versionPublish the CLI only after the matching @cwa-dev/sendkit-core version is already on npm, since bun publish resolves the workspace:* dependency to the current core version at publish time.
Run the CLI publish workflow:
cd packages/cli
bun publish --dry-run
bun publishThe dry run should include README.md, package.json, and compiled files under dist/, including:
dist/index.js
dist/index.d.ts
dist/index.js.map
The dry run should not include src/, node_modules/, or tsconfig.build.json.
After publishing, verify the package metadata and installed binary:
npm view @cwa-dev/sendkit version
npm install -g @cwa-dev/sendkit
sendkit --help
sendkit --version
npm uninstall -g @cwa-dev/sendkitPublish the local MCP package only after the matching @cwa-dev/sendkit-core version is already on npm, since bun publish resolves the workspace:* dependency to the current core version at publish time.
Run the local MCP publish workflow:
cd packages/local-mcp
bun publish --dry-run
bun publishThe dry run should include README.md, package.json, and compiled files under dist/, including:
dist/index.js
dist/index.d.ts
dist/index.js.map
The dry run should not include src/, node_modules/, or tsconfig.build.json.
After publishing, verify the package metadata and installed binary:
npm view @cwa-dev/sendkit-mcp version
npm install -g @cwa-dev/sendkit-mcp
command -v sendkit-mcp
npm uninstall -g @cwa-dev/sendkit-mcpTo manually verify runtime startup, run TELEGRAM_BOT_TOKEN="<bot-token>" sendkit-mcp and stop the stdio server with Ctrl-C.
If bun --filter cannot find a package, run bun install from the repository root and confirm the package name matches the workspace package name.
If TypeScript cannot resolve workspace packages, confirm each package has "type": "module", an exports entry, and a dependency that can resolve locally. Packages can keep "workspace:*" for internal dependencies; bun publish rewrites workspace:* to the concrete published version, so the manifest on npm never contains a workspace range.
If the MCP server appears to hang, that is expected for stdio mode. It waits for MCP client messages until the process is stopped.
If an MCP client cannot start the server, confirm the command is available on PATH, use npx -y @cwa-dev/sendkit-mcp, or use an absolute path in local development config.
If CLI output is difficult to parse in scripts, pass --json and parse stdout as JSON.
If Telegram requests fail from the CLI, run sendkit init and confirm the bot can send messages to the target chat.
If Telegram requests fail from MCP, confirm the MCP client config provides TELEGRAM_BOT_TOKEN in the server environment.