Getting Started
Why this exists
Every agntn package ships the same tools four times. An MCP server, a Pi extension, an OMP extension and an AI SDK tool set. Same names, same arguments, same answers. At least that's the plan.
In practice each host wants its own shape. MCP wants inputSchema and annotations. Pi wants parameters plus a prompt snippet. OMP wants a schema built with its own TypeBox and an approval tier. The AI SDK wants a schema it can validate, which usually meant writing it again in Zod. So the schema lived in four places, and four places drift. A bound fixed in MCP stayed wrong in Pi. An error message sanitized on one surface leaked raw on another.
@agntn/tools is the boring fix. You write the tool once with defineTool. Four small adapters hand it to each host in the shape that host expects. The core validates every call before your code runs, on every host, with the same error text.
Install
pnpm add @agntn/tools
Node.js 24 or newer. TypeBox comes bundled, so there's nothing else to add for the core. The hosts are optional peers: install @modelcontextprotocol/server, @earendil-works/pi-coding-agent, @oh-my-pi/pi-coding-agent or ai only for the ones you actually serve.
First tool
import { defineTool, Type } from "@agntn/tools";
export const slugTool = defineTool({
name: "text_slug",
title: "Text Slug",
description: "Turn a title into a lowercase URL slug.",
effect: "read",
input: Type.Object(
{
text: Type.String({ minLength: 1, maxLength: 200, pattern: "\\S" }),
separator: Type.Optional(Type.Enum(["-", "_", "."])),
},
{ additionalProperties: false },
),
execute({ text, separator = "-" }) {
const slug = text.toLowerCase().match(/[a-z0-9]+/g)?.join(separator) ?? "";
return { content: [{ type: "text", text: slug }], details: { slug } };
},
});
That's a complete tool. The one this site calls everywhere is the same idea with accents handled and an honest failure when nothing is left. Its file is on the landing page, and it really is the file.
Hand it to the hosts
import { createMcpServer } from "@agntn/tools/mcp";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import { slugTool } from "./tools.ts";
const server = createMcpServer({ name: "slugs", version: "1.0.0" }, [slugTool]);
await server.connect(new StdioServerTransport());
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { registerPiTools } from "@agntn/tools/pi";
import { slugTool } from "../../../src/tools.ts";
export default function (pi: ExtensionAPI) {
registerPiTools(pi, [slugTool]);
}
OMP and the AI SDK look the same, one call each. Hosts has every adapter, what it gives its host and the gotchas that host brings along.
What you get for free
- The core checks every call against the schema, even when a host skips its own check.
- A misspelled key is an error that names itself, not a silently dropped argument.
- Error text stays on one line. No forged lines from a hostile tool name, no escape codes in your terminal.
- The OMP TypeBox trap is already handled. Honestly this one alone was worth the package.