Каждый LLM-фреймворк описывает инструмент по-своему. AI SDK.. AI SDK. JavaScript.. AI SDK. JavaScript. llm.. AI SDK. JavaScript. llm. mcp.. AI SDK. JavaScript. llm. mcp. Open source.. AI SDK. JavaScript. llm. mcp. Open source. Standard Schema.. AI SDK. JavaScript. llm. mcp. Open source. Standard Schema. tool calling.. AI SDK. JavaScript. llm. mcp. Open source. Standard Schema. tool calling. TypeScript.. AI SDK. JavaScript. llm. mcp. Open source. Standard Schema. tool calling. TypeScript. искусственный интеллект.

Чтобы дать LLM доступ к своему коду, пишут инструменты (tools). Инструмент — это функция и всё, что модели нужно знать, чтобы её вызвать: имя, описание и схема аргументов.

Потом те же инструменты приходится писать ещё раз. Первая версия сделана на tool() из AI SDK. В проекте появляется MCP-сервер, и инструменты переписывают под registerTool. Соседняя команда использует Genkit, и те же инструменты пишут в третий раз, через defineTool. Функции остаются прежними, меняется только обёртка.

К тому же каждая обёртка привязана к своему фреймворку. tool() берётся из пакета ai, defineTool — метод экземпляра Genkit, registerTool — метод McpServer из MCP SDK. Библиотеке, которая хочет поставлять инструменты, приходится выбрать один фреймворк, и его устанавливает каждый, кто её использует.

Без фреймворка инструмент — это функция, которая описывает сама себя: код, имя, описание и схемы входа и выхода. Модели этого достаточно, чтобы решить, когда и как вызвать инструмент. Этого же хватит, чтобы сгенерировать документацию, построить форму или добавить команду в CLI. Если оформить инструмент обычным объектом с такими полями, он останется частью вашего кода. Чтобы перенести его в другой фреймворк, нужен только небольшой адаптер — переписывать ничего не придётся.

Самое сложное в этом объекте — схемы, и они уже стандартизированы.

Что такое Standard Schema

Standard Schema — TypeScript-интерфейс для библиотек валидации. Его разработали авторы Zod, Valibot и ArkType. Код, который принимает Standard Schema, работает со схемой из любой библиотеки с поддержкой спецификации, и адаптер под каждую библиотеку не нужен.

Весь интерфейс — одно свойство, ~standard. В сокращённом виде:

interface StandardSchemaV1<Input = unknown, Output = Input> {
  readonly '~standard': {
    readonly version: 1;
    readonly vendor: string;
    readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
    readonly types?: { readonly input: Input; readonly output: Output };
  };
}

// Result<Output> — это { value: Output } при успехе и { issues: Issue[] } при ошибке.

Код валидации одинаковый для любой библиотеки:

const result = await schema['~standard'].validate(data);
if (result.issues) throw new Error(result.issues.map((issue) => issue.message).join('; '));
const value = result.value; // тип берётся из output схемы

Спецификацию реализуют больше 30 библиотек, среди них Zod, Valibot, ArkType, yup и joi. Принимают её больше 60 проектов, в том числе tRPC, TanStack Form и Router, Hono, Elysia, oRPC и React Hook Form.

Спецификация состоит только из типов. В пакете @standard-schema/spec нет рантайм-кода, и библиотека может скопировать интерфейс к себе, а не зависеть от пакета.

Что такое Standard JSON Schema

Валидация — только половина того, что инструменту нужно от схем. Вторая половина — JSON Schema: прежде чем вызвать инструмент, модель должна получить JSON Schema его аргументов.

Standard JSON Schema — вторая спецификация тех же авторов. Она добавляет в то же свойство ~standard конвертер в JSON Schema:

schema['~standard'].jsonSchema.input({ target: 'draft-2020-12' });
schema['~standard'].jsonSchema.output({ target: 'openapi-3.0' });

target выбирает диалект JSON Schema, потому что разным потребителям нужны разные. OpenAI, Anthropic и MCP принимают JSON Schema draft 2020-12, а поле parameters у Gemini — формат OpenAPI 3.0.

input и output разделены, потому что схема может преобразовывать значения. Если схема принимает "42" и возвращает 42, у её входа одна JSON Schema, а у выхода — другая.

Спецификации независимы: объект может реализовать любую из них или обе. Схемы Zod 4.2+ и ArkType 2.1.28+ реализуют обе. В Valibot 1.2+ схему оборачивают в toStandardJsonSchema() из @valibot/to-json-schema, и тогда она тоже реализует обе.

Что такое Standard Tool

Когда схемы сами валидируют данные и выдают JSON Schema, от инструмента остаются имя, описание и функция. Для этой части стандарта нет, поэтому каждый фреймворк определяет для неё свой объект.

StandardToolV0 — предложение, каким должен быть этот объект:

import type { StandardSchemaV1, StandardJSONSchemaV1 } from '@standard-schema/spec';

interface StandardToolV0<
  Input = unknown, Output = unknown, FormattedOutput = Output, Context = unknown,
> {
  name: string;
  title?: string;
  description: string;
  inputSchema?: StandardSchemaV1<Input, unknown> & StandardJSONSchemaV1<Input, unknown>;
  outputSchema?: StandardSchemaV1<unknown, Output> & StandardJSONSchemaV1<unknown, Output>;
  meta?: Record<string, unknown>;
  execute(input: Input, context?: Context): FormattedOutput | Promise<FormattedOutput>;
}
  • name — идентификатор, по которому модель вызывает инструмент.

  • description объясняет модели, что делает инструмент и когда его вызывать.

  • title — необязательное название для людей. MCP-клиенты могут показывать его в списке инструментов.

  • inputSchema и outputSchema должны реализовывать обе спецификации, то есть и валидировать, и выдавать JSON Schema. Input — входной тип inputSchema, а Output — выходной тип outputSchema, поэтому подходят и схемы, которые преобразуют значения.

  • meta — статические данные об инструменте, например { destructive: true }. Их читают потребители, а execute их не получает.

  • execute выполняет инструмент. Необязательный второй аргумент, context, передаёт данные конкретного вызова, например локаль или токен авторизации. context не валидируется и не попадает в JSON Schema.

  • FormattedOutput — то, что возвращает execute, если обёртка меняет результат, например чтобы возвращать ошибки как данные. По умолчанию это Output.

Как и обе спецификации, StandardToolV0 — это только тип. Подходит любой объект с этими полями:

import { z } from 'zod'; // или ArkType, или Valibot
import type { StandardToolV0 } from 'standard-tool';

export const getWeather: StandardToolV0<{ city: string }, { tempC: number }> = {
  name: 'get_weather',
  description: 'Current temperature for a city',
  inputSchema: z.object({ city: z.string() }),
  outputSchema: z.object({ tempC: z.number() }),
  execute: async ({ city }) => ({ tempC: await fetchTemperature(city) }),
};

Импорт здесь только для типов, и вместо него можно вставить интерфейс прямо в свой проект.

В пакете standard-tool есть и необязательная эталонная реализация, около 90 строк. standardTool() оборачивает определение так, что execute проверяет вход до вызова вашей функции и выход после, а при несовпадении бросает StandardToolValidationError. withFormattedOutput() перехватывает ошибки и возвращает их как данные, чтобы модель могла прочитать, что пошло не так.

Сравнение с фреймворками

Такой объект есть в каждом фреймворке. Отличаются в основном названия и позиции аргументов:

Пакет

Идентификатор

Схема входа

Схема выхода

Функция

AI SDK

ai

ключ в объекте tools

inputSchema

outputSchema

execute

Mastra

@mastra/core

id

inputSchema

outputSchema

execute

Genkit

genkit

name

inputSchema

outputSchema

2-й аргумент defineTool

LangChain

@langchain/core

name

schema

нет

1-й аргумент tool

MCP SDK

@modelcontextprotocol/sdk

1-й аргумент registerTool

inputSchema

outputSchema

3-й аргумент registerTool

StandardToolV0

нет, это тип

name

inputSchema

outputSchema

execute

Сильнее различается то, что фреймворки принимают в качестве схемы:

  • AI SDK: Standard Schema, Zod или JSON Schema

  • Mastra: Standard Schema вместе со Standard JSON Schema, Zod или JSON Schema

  • Genkit: Zod или JSON Schema

  • LangChain: Zod или JSON Schema

  • MCP SDK: только Zod

Проверено на версиях ai 7.0, @mastra/core 1.72, genkit 1.42, @langchain/core 1.2 и @modelcontextprotocol/sdk 1.31.

Объекты похожи, но не взаимозаменяемы, и каждому нужен пакет своего фреймворка. Как правило, чтобы перенести инструмент в другой фреймворк, обёртку приходится переписывать. Mastra — исключение, и то в одну сторону: её агенты принимают и инструменты из AI SDK. А чтобы использовать инструмент, написанный под другой фреймворк, нужно установить этот фреймворк.

Это важно даже при одном фреймворке. Объект инструмента рассчитан на свой фреймворк. Обычный объект можно ещё и вызвать из скрипта или теста, прочитать генератором документации или экспортировать из библиотеки, пользователи которой ваш фреймворк не ставят.

Как это использовать

У объекта не один читатель, и модель — только один из них.

Вызвать. execute — обычная функция:

const { tempC } = await getWeather.execute({ city: 'Paris' });

Отдать модели. name, description и JSON Schema, которую выдаёт inputSchema, превращаются в описание инструмента для провайдера. Когда модель вызывает инструмент, её аргументы уходят в execute. Как это выглядит у разных провайдеров — в следующем разделе.

Прочитать. Полей хватает для справочной документации, списка инструментов в промпте, формы по inputSchema или команды CLI:

function describeTools(tools: StandardToolV0[]) {
  return tools.map((tool) => ({
    name: tool.name,
    description: tool.description,
    input: tool.inputSchema?.['~standard'].jsonSchema.input({ target: 'draft-2020-12' }),
    output: tool.outputSchema?.['~standard'].jsonSchema.output({ target: 'draft-2020-12' }),
  }));
}

Экспортировать из библиотеки. Инструменты — обычные значения, и библиотека экспортирует их как любые другие:

export const getOrders: StandardToolV0<{ userId: string }, Order[]> = {
  name: 'get_orders',
  description: "List a user's orders",
  inputSchema: z.object({ userId: z.string() }),
  execute: ({ userId }) => api.get(`/orders/${userId}`),
};

Пользователи библиотеки могут запустить инструмент, задокументировать его или отдать модели, а сама библиотека не зависит ни от одного AI-фреймворка.

Взять готовые RPC-процедуры. У процедуры tRPC или oRPC уже есть схемы входа и выхода и обработчик. Если схемы реализуют Standard JSON Schema, они становятся схемами инструмента, а execute вызывает процедуру через серверный вызов фреймворка (в tRPC это createCaller). Пример с tRPC.

Подключение к моделям и фреймворкам

Любая интеграция делает две вещи. Сначала собирает описание инструмента для провайдера из name, description и JSON Schema. Потом, когда модель вызывает инструмент, запускает execute и отправляет результат обратно. От провайдера к провайдеру меняются только названия полей и диалект JSON Schema:

Потребитель

Поле схемы

target

Как возвращается результат

OpenAI Responses API

parameters

draft-2020-12

элемент function_call_output

Anthropic

input_schema

draft-2020-12

блок tool_result

Gemini

parameters

openapi-3.0

часть functionResponse

MCP

inputSchema в дескрипторе инструмента

draft-2020-12

{ content, structuredContent?, isError? }

AI SDK

inputSchema, принимает Standard Schema как есть

не нужен

SDK сам ведёт цикл

Обе части на примере Anthropic:

import type Anthropic from '@anthropic-ai/sdk';
import type { StandardToolV0 } from 'standard-tool';

export function toAnthropicTool(tool: StandardToolV0): Anthropic.Tool {
  const schema = tool.inputSchema?.['~standard'].jsonSchema.input({ target: 'draft-2020-12' });
  return {
    name: tool.name,
    description: tool.description,
    input_schema: (schema ?? { type: 'object', properties: {} }) as Anthropic.Tool.InputSchema,
  };
}

export async function runToolUse(
  tools: StandardToolV0[],
  block: Anthropic.ToolUseBlock,
): Promise<Anthropic.ToolResultBlockParam> {
  try {
    const tool = tools.find((t) => t.name === block.name);
    if (!tool) throw new Error(`Unknown tool: ${block.name}`);
    const result = await tool.execute(block.input);
    return { type: 'tool_result', tool_use_id: block.id, content: JSON.stringify(result) };
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    return { type: 'tool_result', tool_use_id: block.id, content: message, is_error: true };
  }
}

execute получает аргументы модели без проверки. Инструмент из standardTool() проверяет их по inputSchema, а в написанном вручную это нужно делать самому.

Для другого провайдера поменяйте поле и target по таблице. Адаптер пишется один раз, и новый провайдер не требует правок в инструментах.

Итог

Инструмент, оформленный как функция, которая описывает сама себя, остаётся частью вашего кода. Его можно вызвать, протестировать, задокументировать и отдать любой модели или фреймворку, и это будет всё тот же объект.

StandardToolV0 — это предложение: один TypeScript-интерфейс без рантайма. Форма V0 заморожена, так что правки, которые её меняют, пойдут в новый интерфейс — StandardToolV1. Спецификация, эталонная реализация и обоснование — на standard-tool.js.org.

Очевидное возражение — XKCD 927: пока другие проекты не создают и не читают такие объекты, это просто ещё один конкурирующий формат. Standard Schema показала, что маленький интерфейс без рантайма может широко распространиться, но за ней с самого начала стояли авторы Zod, Valibot и ArkType. У этого предложения один мейнтейнер и такой поддержки нет.

Автор: Finom

Источник