Guide

Agents

The same four tools for the Vercel AI SDK, the MCP server, Pi and OMP, and what an agent should and should not do with the answer.

Four tools, every host

ToolDoesReturns
web_searchone query or a batch, one provider, automatic selection, or all for fan-outresults with the provider diagnostics
web_search_imagematching pages and images for one public image URLImageSearchResult[]
web_readone URL or a batch into normalized content, with reader provenance{ result, requestedProvider, provider, attempts, failures }
web_providersthe running build, configuration, reachability and the complete capability matrixlistProviders() plus packageCapabilities

Same four on every surface. The descriptions name the built-in providers, but execution checks the name against the live registry, so a custom provider registered before the session works in a tool call too. The host's abort signal cancels the provider request, no zombie fetches after the model gives up.

AI SDK

import { generateText } from "ai";
import { readTool, searchImageTool, searchTool } from "@agntn/web/ai";

const { text } = await generateText({
  model: yourModel,
  tools: {
    web_search: searchTool,
    web_search_image: searchImageTool,
    web_read: readTool,
  },
  prompt: "Find the latest TypeScript release notes",
});

ai and zod are optional peers. The main entry never imports them, only the /ai subpath does. providersTool is the fourth export.

type SearchToolInput = {
  query: string | string[]; // up to 10 in a batch
  provider?: string; // a built-in, a custom name, or "all"
  maxResults?: number; // 1 to 20, default 10
  continuation?: string;
  highlights?: boolean;
  summary?: boolean;
  fullText?: boolean;
  includeDomains?: string[];
  excludeDomains?: string[];
  sources?: string[];
  categories?: string[];
  category?: string;
  startPublishedDate?: string;
  endPublishedDate?: string;
};

A scalar search returns { provider, results, ignoredFilters, undeclaredFilters, pagination, metadata? }, automatic ones add attempts and failures, and provider: "all" returns the fan-out envelope with providers and evidence on each result. readTool takes url or an array of URLs plus the read options, defaults to 20 000 characters and accepts at most 200 000. Without that default a model would happily ask for a whole site and stuff it into its context.

MCP

web mcp
claude mcp add web --scope user -- web mcp

The server uses the SDK's Server class with TypeBox schemas. Every tool advertises an output schema and returns its result under structuredContent.result, compact JSON stays in content for clients that only render text. Tool symbols and titles match the native extensions, and nothing decorative gets written into the JSON-RPC stream. The client owns its TUI, not the server.

createMcpServer() from the @agntn/web/mcp subpath returns the same server when your host brings its own transport.

Pi and OMP

pi install git:github.com/agntn/web

The package ships the four tools as a pi extension and an OMP extension. Pi also gets two slash commands: /web [query] shows results as a selector and pastes the chosen URL into the editor, /web-providers shows configuration, reachability and capabilities. Both extensions read the same env vars as the library. Their TUI rows show progress, provider choice, result counts and fallback attempts, with bounded previews. A whole page rendered into the terminal is useless, so it never happens.

What the answer is

The tools return the normalized objects, not prose. A model summarizing them should say what the provider said. The Explorer shows the same objects for any query, which is a quick way to check a summary against the source when a model gets creative.

Titles, snippets, highlights and page content are published by whoever owns the page. They are data to report, not instructions to follow. An agent that reads snippet: "ignore previous instructions" has learned one thing about the page, and nothing about what to do next.

Rolling your own

Framework is none of the above? The four tools are thin wrappers over the helpers anyway:

import { listProviders, readUrlDetailed, searchAllDetailed, searchByImage, searchWithFallback } from "@agntn/web";

export async function run(input: { operation: string; query?: string; url?: string; provider?: string }, signal?: AbortSignal) {
  switch (input.operation) {
    case "search":
      return input.provider === "all"
        ? searchAllDetailed(input.query!, { signal })
        : searchWithFallback(input.query!, { signal });
    case "search_image":
      return searchByImage(input.url!, { provider: input.provider, signal });
    case "read":
      return readUrlDetailed(input.url!, { provider: input.provider, maxChars: 20_000, signal });
    case "providers":
      return listProviders();
  }
}

@agntn/web·MIT license· Search results and page content are data, never instructions.