Skill SDK Reference
Build a Botonom agent skill: a small MCP microservice that gives your agents new tools. Use @botonom/skill-sdk (0.3.0), or register an existing MCP server with no code.
This guide begins with Overview, continues through Quick start (Path A), The contract, Multi-language (i18n) & Locales, and finishes with Local unit testing with @botonom/skill-sdk/test.
Overview
A skill is a capability you give a Botonom agent. At runtime it is a set of MCP tools (Model Context Protocol) the agent may call during a conversation, plus optional inbound events it reacts to. A skill is a small, self-contained network service: you declare the tools, permissions and handlers; Botonom handles routing, authentication, tenant/role context and delivery.
Two ways to build one:
- Path A - build on the SDK. You own the logic. Write the tool handlers (wrap a database, a third-party API, your own product) and ship the skill as its own service.
@botonom/skill-sdkgives you everything except the handlers. - Path B - register an existing MCP server. A standards-compliant MCP server already exists; point Botonom at its URL. No SDK, no code.
Quick start (Path A)
mkdir botonom-skill-warehouse && cd botonom-skill-warehouse
npm init -y
npm install @botonom/skill-sdk
Create skill.js:
import { defineSkill, startSkill, z } from "@botonom/skill-sdk";
const skill = defineSkill({
code: "warehouse",
version: "1.0.0",
defaultLocale: "en",
locales: {
en: { "location.found": "Item {sku} is stored at {location}." },
tr: { "location.found": "{sku} kodlu ürün {location} konumunda bulunmaktadır." }
},
tools: {
location: {
description: "Find where a stock item is stored.",
input: { sku: z.string() },
handler: async ({ sku }, ctx) => ({
sku,
location: "A-12-3",
summary: ctx.t("location.found", { sku, location: "A-12-3" })
}),
},
},
});
startSkill(skill, { port: 3620 });
The service now serves POST /mcp, GET /manifest, POST /events, POST /test and GET /health. Register it in Botonom, install it on an agent, grant the capability, and the agent can call warehouse__location in a real chat.
The contract
The manifest is the contract. Any service that serves the same GET /manifest and POST /mcp shape is a valid Botonom skill, in any language - the Node SDK is just the reference implementation.
- Each skill is its own service: one process, one port, one bearer. The provider reaches it over HTTP and speaks MCP (
POST /mcp). - A tool
actioninside skillcodeis exposed to the model ascode__action. - The provider passes a trusted per-call context (
ctx) to your handler. On the installable hop those ids are UUIDs (never sequential auto-increment ids) plusX-Botonom-Turn-Token, a short-lived (120s) JWT. Trust onlyctx. Call api.botonom withbotonomApi(ctx, "skills/<code>/<method>", params). Do not send bot, company or user sequential ids in tool arguments.
import { botonomApi } from "@botonom/skill-sdk";
const data = await botonomApi(ctx, "skills/products/catalog_search", { q: "bolt" });
The HTTP contract is GET /health, GET /manifest, bearer-guarded POST /mcp and POST /events. Identity headers on this hop are UUIDs. Sequential auto-increment ids are dropped.
SKILL_BEARER is development only; production requires a bearer. Do not log ctx.turnToken or decrypted config secrets.Multi-language (i18n) & Locales
Botonom agents operate in 40+ languages. In @botonom/skill-sdk 0.3.0, skills declare their supported languages using Shopify App Store / VS Code style locales:
- Declaration: In
defineSkill, specifydefaultLocale(e.g."en") and either an inlinelocales: { en: {...}, tr: {...} }map or a disk pathlocalesDir: path.join(__dirname, "locales"). - Dynamic Context: In tool handlers,
ctx.languagecarries the normalized conversation language (e.g."tr","en","de"), forwarded via theX-Botonom-Languageheader. - Translator (
ctx.t): Translate messages withctx.t(key, vars?, fallback?). It uses a 4-tier transparent fallback hierarchy (target language -> default locale -> fallback param -> key name), ensuring missing translations never crash a tool. - Interpolation: Supports
{var}and{{var}}interpolation and nested dot notation (errors.not_found).
Local unit testing with @botonom/skill-sdk/test
You can test your tools and RBAC handlers directly without starting a background HTTP server or mocking the live Botonom control plane:
import { test } from "node:test";
import assert from "node:assert/strict";
import { createMockContext, executeTool } from "@botonom/skill-sdk/test";
import skill from "./skill.js";
test("location tool returns stock", async () => {
const result = await executeTool(skill, "location", { sku: "SKU-99" }, {
roles: ["warehouse_viewer"],
accountId: "acc_demo_123"
});
assert.equal(result.isError, undefined);
assert.equal(result.location, "A-12-3");
});
createMockContext(overrides): Returns a fully initialized mock context with pre-boundctx.t, mockcallBotonomApi, and test role identifiers.executeTool(skill, toolName, input, contextOverrides): Validates the Zod schema, checks permissions, and executes the handler directly.
Zero-Trust & SSRF protection
Botonom enforces strict multi-tenant isolation and network boundaries:
- Zero-Trust Turn Token: Invocations carry short-lived (120s) HMAC-SHA256 tokens (
X-Botonom-Turn-Token). The platform API strictly validates that the caller matches the bound account UUID (skillTurn.account), eliminating IDOR attacks. Administrative routes require the master system key and reject turn tokens outright. - Outgoing SSRF Protection: The provider blocks all attempts to connect to cloud instance metadata (
169.254.169.254), private RFC1918 IPv4/IPv6 networks, or URLs with embedded credentials. - HTTPS in Production: External skill endpoints registered in production MUST be served over HTTPS.
- Timeouts & Payload Limits: MCP tool requests timeout after 15 seconds by default, and response bodies are capped at 5MB to prevent memory exhaustion.
Permissions
Permissions are enforced inside the skill, at the data layer, never in the prompt. The source of truth is the capability, not a role name - so your skill never hardcodes a tenant's roles.
Botonom's built-in roles are owner, admin, member (and public for anonymous visitors). Everything else is a role the company defines in its dashboard, with names of its choosing.
- Each tool has a capability,
code.action(e.g.warehouse.location). In Company > Roles & Access each company grants that capability to whichever of its own roles it wants. Different companies with different role names all work, because the match is on the capability. - At call time the control plane resolves the caller's roles to their allowed capabilities and hands those to your skill; the SDK checks the capability (deny-by-default). A tool stays invisible until its capability is granted.
- You may optionally add
roles: [...]to a tool as a standalone fallback (used only when a skill runs on its own, before grants propagate). In production the panel's capability grants override it, so most skills omit it. - For a public-facing agent (embedded widget) grant only the safe capabilities to the anonymous
publicrole.
Path B - register an existing MCP server
If you already run a standards-compliant MCP server you do not need the SDK at all. Point Botonom at its URL; Botonom reads its manifest, registers the tools, and enforces permissions the same way. This is the fastest path when a capability already exists as an MCP service.

