The Vibes community toolset ecosystem lets you package reusable tool groups as independent libraries and share them via JSR or npm. Any toolset that implements the Toolset<TDeps> interface can be plugged into any Vibes agent — no special framework coupling required.
The three toolsets bundled inside @vibesjs/sdk (TodoToolset, SkillsToolset, MemoryToolset) follow exactly the same patterns described here. They are community contributions that were promoted into the core package. Your own published toolset is structurally identical.
A toolset is a good candidate for extraction if it:
- Groups tools that share a single domain (task tracking, web browsing, file I/O, etc.)
- Maintains its own state that benefits from a replaceable storage interface
- Can be configured with options that vary across deployments
- Has no hard dependency on a specific agent or model
my-toolset/
├── deno.json # package metadata, exports map
├── mod.ts # re-exports everything public
├── toolset.ts # the Toolset<TDeps> class
├── types.ts # public interfaces (Store, Config, etc.)
├── store.ts # default in-memory store implementation
└── tests/
└── toolset_test.ts
deno.json (JSR)
{
"name": "@yourscope/my-toolset",
"version": "0.1.0",
"exports": {
".": "./mod.ts"
}
}
For npm: use a standard package.json with "exports" pointing at the compiled entry.
mod.ts — public surface
export { MyToolset } from "./toolset.ts";
export type { MyToolsetConfig, MyStore } from "./types.ts";
types.ts — interfaces
Define a storage interface so consumers can swap in their own persistence layer:
export type MyItem = {
id: string;
content: string;
createdAt: Date;
};
export interface MyStore {
add(content: string): Promise<MyItem>;
list(): Promise<MyItem[]>;
delete(id: string): Promise<boolean>;
}
export type MyToolsetConfig = {
store?: MyStore;
};
store.ts — default in-memory implementation
import type { MyItem, MyStore } from "./types.ts";
export class InMemoryMyStore implements MyStore {
private _items = new Map<string, MyItem>();
private _nextId = 1;
async add(content: string): Promise<MyItem> {
const id = String(this._nextId++);
const item: MyItem = { id, content, createdAt: new Date() };
this._items = new Map(this._items).set(id, item); // immutable update
return item;
}
async list(): Promise<MyItem[]> {
return [...this._items.values()];
}
async delete(id: string): Promise<boolean> {
if (!this._items.has(id)) return false;
const next = new Map(this._items);
next.delete(id);
this._items = next;
return true;
}
}
import { FunctionToolset, tool, type ToolDefinition } from "jsr:@vibesjs/sdk";
import type { Toolset } from "jsr:@vibesjs/sdk";
import { z } from "npm:zod";
import { InMemoryMyStore } from "./store.ts";
import type { MyStore, MyToolsetConfig } from "./types.ts";
export class MyToolset<TDeps = undefined> implements Toolset<TDeps> {
private readonly _store: MyStore;
private readonly _inner: FunctionToolset<TDeps>;
constructor(storeOrConfig?: MyStore | MyToolsetConfig) {
if (storeOrConfig && "add" in storeOrConfig) {
this._store = storeOrConfig as MyStore;
} else {
this._store = (storeOrConfig as MyToolsetConfig)?.store ?? new InMemoryMyStore();
}
this._inner = new FunctionToolset<TDeps>(this._buildTools());
}
tools(ctx: Parameters<Toolset<TDeps>["tools"]>[0]) {
return this._inner.tools(ctx);
}
private _buildTools(): ToolDefinition<TDeps>[] {
return [
tool<TDeps>({
name: "my_add",
description: "Add a new item",
parameters: z.object({ content: z.string() }),
execute: async (_ctx, { content }) => {
const item = await this._store.add(content);
return JSON.stringify(item);
},
}),
tool<TDeps>({
name: "my_list",
description: "List all items",
parameters: z.object({}),
execute: async () => {
const items = await this._store.list();
return JSON.stringify(items);
},
}),
tool<TDeps>({
name: "my_delete",
description: "Delete an item by ID",
parameters: z.object({ id: z.string() }),
execute: async (_ctx, { id }) => {
const removed = await this._store.delete(id);
return JSON.stringify({ removed });
},
}),
];
}
}
Publishing to JSR
# One-time: log in
deno publish --dry-run # verify what will be published
# Publish
deno publish
After publishing, consumers import like this:
import { MyToolset } from "jsr:@yourscope/my-toolset";
const agent = new Agent({
model,
toolsets: [new MyToolset()],
});
Publishing to npm
Compile to JavaScript (e.g. with deno bundle or esbuild), then:
Naming
- Package name:
@scope/vibes-<domain>-toolset (e.g. @acme/vibes-crm-toolset)
- Tool names:
snake_case with a descriptive verb prefix (e.g. crm_search, crm_update)
- Avoid generic names like
search or get — they collide with other toolsets
Storage interfaces
Always define a <Domain>Store interface and provide a default InMemory<Domain>Store. This lets consumers plug in Redis, PostgreSQL, or any other store without forking your code.
Generic over TDeps
Make your toolset class generic over TDeps even if the tools themselves do not use ctx.deps:
export class MyToolset<TDeps = undefined> implements Toolset<TDeps> {
This lets consumers use your toolset alongside agents that have typed dependencies, without type errors.
Dependency injection
If your tools need runtime configuration (API keys, base URLs), accept them through the constructor — never hardcode or read directly from Deno.env inside the toolset. Let the consumer decide how configuration is supplied.
// Good
export class SearchToolset<TDeps = undefined> implements Toolset<TDeps> {
constructor(private readonly apiKey: string) {}
}
// Avoid
execute: async () => {
const key = Deno.env.get("SEARCH_API_KEY"); // ties the toolset to Deno
}
Error handling
Return structured error strings from execute rather than throwing for expected failures (tool not found, empty result). Throw for unexpected/unrecoverable errors only — the SDK will propagate those to the caller.
execute: async (_ctx, { id }) => {
const item = await this._store.get(id);
if (!item) return JSON.stringify({ error: `Item ${id} not found` });
return JSON.stringify(item);
},
Testing
Use TestModel from @vibesjs/sdk to write unit tests without making real model calls:
import { Agent, TestModel, setAllowModelRequests } from "jsr:@vibesjs/sdk";
import { MyToolset } from "./toolset.ts";
setAllowModelRequests(false);
Deno.test("MyToolset.my_add stores and returns an item", async () => {
const toolset = new MyToolset();
const model = new TestModel([
{
toolCalls: [{ toolName: "my_add", args: { content: "hello" } }],
},
{ output: "done" },
]);
const agent = new Agent({ model, toolsets: [toolset] });
await agent.run("add hello");
});
Documentation
- Write a
README.md with a quick-start example
- Document every tool name, parameter, and return value
- Note provider or runtime dependencies (e.g. “requires Deno”, “requires a browser environment”)
The three toolsets bundled inside @vibesjs/sdk live in packages/sdk/community/ in the vibes repository. To add a new bundled toolset:
- Create
packages/sdk/community/<name>/ with types.ts, implementation, and mod.ts.
- Export from
packages/sdk/community/mod.ts and packages/sdk/mod.ts.
- Add tests in
packages/sdk/tests/community_<name>_test.ts.
- Add a doc page at
packages/sdk/docs/community/<name>.mdx.
- Open a PR with the title
feat(community): add <YourToolset>.
The port-pydantic-ai-community-plugins agent skill provides step-by-step guidance for porting any pydantic-ai plugin:
mkdir -p .claude/agents && curl -fsSL \
https://raw.githubusercontent.com/a7ul/vibes/main/packages/sdk/skills/port-pydantic-ai-community-plugins.md \
-o .claude/agents/port-pydantic-ai-community-plugins.md