octools
Version: 1.2.1 ยท Date: 2026-10-08
octoolsis the OpsCogs standard interface for AI agent tools, in Python and TypeScript.
It holds the OCTool descriptor, the result envelopes every tool returns,
a conformance validator, and plain-data converters to the MCP and
Anthropic tool shapes. A library describes each capability once as an
OCTool; any agent runtime registers it without re-expressing it by hand.
Think of it as OpenAPI for agent tools: one vendor-neutral contract that every OpsCogs library fills in, so the interface is defined once and each adopter only supplies its tools. The contract ships as two packages with the same API, one per language:
| Language | Package | Runtime | Only dependency |
|---|---|---|---|
| Python | octools on PyPI |
Python 3.12 or newer | pydantic >= 2.12, < 3 |
| TypeScript | @opscogs/octools on npm |
Node 22.18 or newer, ESM only | zod 4, a peer dependency |
Neither package depends on an OpsCogs package or an agent SDK, so the
dependency arrow always points toward octools, never back. The source
for both lives in one repository:
github.com/opscogs/octools.
- The OCTool descriptor - every field, the envelopes, the escape conditions, strict-clean rules, versioning.
- Converters - the MCP and Anthropic mappings, what is lossless and what is dropped.
- Listing tools for people - the
octools listcommand and the listing API behind it. - The TypeScript package - runtime floor, the Python to TypeScript name map, and where the two languages differ.
- TypeScript API - the generated reference
for every export of
@opscogs/octools.
Code examples on these pages come in both languages. Pick a tab once and every page follows it.
Install
Python 3.12 or newer and pydantic >= 2.12, < 3 (the only dependency).
pip install octools
Node 22.18 or newer; the package is ESM only and ships its type declarations. zod 4 is a peer dependency, so install it beside the package.
npm install @opscogs/octools zod
One contract, two languages
The wire format is defined once, outside either package, in the
repository's spec/
directory:
vocabulary.json-SPEC_VERSION, every closed vocabulary with its declared fallback, and the shared constants (name patterns, sentence limits, the strict-mode keyword and format sets).schema/*.schema.json- the JSON Schemas of the descriptor and the envelopes.fixtures/<operation>/<case>.json- golden cases, one per file, each with aninputand theexpectedresult. An error or a validator finding is data inexpected, never a language's exception type.
Both packages' test suites run every fixture and pass all of them; none
is skipped or filtered by language. Conforming to octools means passing
spec/, so the same envelope, the same findings and the same converter
output come back whichever package produced them. Wire keys are spelled
exactly as the spec spells them in both (column_kinds, next_cursor,
inputSchema); only the API names follow each language's idiom.
Versions move in lockstep. One release tag publishes both packages at
the same version, recorded in one changelog, and SPEC_VERSION is the
same in both. A pre-release is spelled 1.2.0rc1 on PyPI and 1.2.0-rc.1
on npm, where it is published under the next tag and never installed by
default. The compatibility contract in
Versioning and deprecation
applies to both registries identically: a consuming library declares a
range from the oldest release carrying every name it uses up to the next
major.
Quick start
Describe the arguments and result as models, wrap the function in an
OCTool, and list the tools from the provider function (all_tools() in
Python, allTools() in TypeScript):
from pydantic import BaseModel, Field
from octools import OCTool, RecordsEnvelope, input_schema_for, records_schema
class EchoArgs(BaseModel):
values: list[str] = Field(description="Strings to echo back, one record each.")
class EchoRecord(BaseModel):
value: str = Field(description="The echoed string.")
position: int = Field(description="Zero-based position in the input.")
def echo_records(_dependency, *, values: list[str]) -> dict:
records = [EchoRecord(value=v, position=i) for i, v in enumerate(values)]
return RecordsEnvelope[EchoRecord](
records=records, count=len(records), truncated=False
).model_dump(mode="json")
ECHO_RECORDS = OCTool(
name="echo_records",
description="Return each input string as one record with its position. Use it to check a tool round-trip. Returns a records envelope.",
input_schema=input_schema_for(EchoArgs),
output_schema=records_schema(EchoRecord),
func=echo_records,
access="local",
)
def all_tools() -> tuple[OCTool, ...]:
return (ECHO_RECORDS,)
// src/my_tools.ts
import * as z from "zod";
import {
OCTool,
RecordsEnvelope,
inputSchemaFor,
recordsSchema,
} from "@opscogs/octools";
const EchoArgs = z.object({
values: z.array(z.string()).describe("Strings to echo back, one record each."),
});
const EchoRecord = z
.object({
value: z.string().describe("The echoed string."),
position: z.int().describe("Zero-based position in the input."),
})
.meta({ id: "EchoRecord" });
function echoRecords(_dependency: unknown, args: unknown): RecordsEnvelope {
const { values } = EchoArgs.parse(args);
const records = values.map((value, position) => ({ value, position }));
return RecordsEnvelope(EchoRecord).parse({
records,
count: records.length,
truncated: false,
});
}
export const ECHO_RECORDS = new OCTool({
name: "echo_records",
description:
"Return each input string as one record with its position. Use it to check a tool round-trip. Returns a records envelope.",
inputSchema: inputSchemaFor(EchoArgs),
outputSchema: recordsSchema(EchoRecord),
func: echoRecords,
access: "local",
});
export function allTools(): readonly OCTool[] {
return [ECHO_RECORDS];
}
.meta({ id }) puts the record model in $defs, as pydantic does for
a nested model, so both packages emit the same output schema. A parse
returns the envelope's wire form, ready for JSON.stringify.
Assert conformance in your own test suite, not at import time:
The converters then give a runtime the registration objects it needs:
to_mcp_tool(ECHO_RECORDS) and to_anthropic_tool(ECHO_RECORDS,
name_prefix="demo_") in Python, toMcpTool(ECHO_RECORDS) and
toAnthropicTool(ECHO_RECORDS, { namePrefix: "demo_" }) in TypeScript;
see Converters.