
Every other integration in this series hands your framework an array or an object of tools. This one is different, because the Claude Agent SDK expects tools to arrive as an MCP server.
That sounds like more work, and I assumed it would be. It is actually less, because the package ships a server already built. You import it, name it in your query() options, and Claude has web access. There is no separate process to run and no MCP config file to maintain.
What you need
- A Node project (the package ships dual ESM and CJS builds)
- A Tabstack API key from console.tabstack.ai
- An Anthropic API key
npm install @tabstack/claude-agent @anthropic-ai/claude-agent-sdk zodexport TABSTACK_API_KEY="your-tabstack-key"
export ANTHROPIC_API_KEY="your-anthropic-key"One version constraint to note. The Claude Agent SDK depends on Zod 4, so this package's zod peer dependency is ^4.0.0. Unlike the eve and LlamaIndex adapters, Zod 3 is not an option here. If your app is still on Zod 3, that upgrade is a prerequisite rather than a nice-to-have.
Step 1: wire in the server
import { query } from "@anthropic-ai/claude-agent-sdk";
import { tabstackServer, tabstackAllowedTools, tabstackMcpServerName } from "@tabstack/claude-agent";
for await (const message of query({
prompt: "What are Vercel's current pricing plans, with sources?",
options: {
mcpServers: { [tabstackMcpServerName]: tabstackServer },
allowedTools: tabstackAllowedTools,
},
})) {
if (message.type === "result" && message.subtype === "success") {
console.log(message.result);
}
}Three imports, and Claude can read the web.
Note the for await. query() returns a lazy async generator rather than a promise, so nothing runs until you iterate it. Writing await query({ ... }) typechecks fine and does absolutely nothing, which is a quiet half hour to lose.
tabstackServer is a ready-built in-process MCP server holding all five tools. In-process is the important word. It runs inside your Node process, so there is no subprocess to spawn, no port to bind, and no server lifecycle to manage. If your only previous experience of MCP was editing a JSON config and restarting something, this is a pleasant surprise.
tabstackAllowedTools pre-approves every Tabstack tool. The Claude Agent SDK gates tool use by permission, so without this the tools exist but Claude cannot call them. It is the line people forget, and the symptom is an agent that talks about wanting to look something up and then does not.
tabstackMcpServerName is the string "tabstack". Use the export rather than typing the literal, because the name becomes part of how Claude addresses each tool.
The server resolves TABSTACK_API_KEY lazily on first tool call, so importing the package never requires a key to be set.
Step 2: understand what Claude actually sees
Claude does not see research_question. It sees mcp__tabstack__research_question.
MCP tools are namespaced by their server, so every tool arrives fully qualified as mcp__tabstack__<tool_name>. This matters in two places. When you read a transcript and want to know which tool ran, that prefix is what you are scanning for. And when you write instructions referring to a tool by name, the unqualified name is what Claude maps onto, but the qualified name is what appears in permission prompts and logs.
Underneath the prefix, the names are the same everywhere:
| Tool name | What it does |
|---|---|
extract_structured_data | Pull specific fields from a URL into a JSON shape you define |
extract_page_content | Fetch a page as clean markdown |
research_question | Synthesised answer with cited sources across multiple pages |
generate_structured_data | Fetch a page, then AI-transform it into derived or reshaped JSON |
automate_browser_task | Run a multi-step, natural language browser task |
Those names are shared with the LangChain, Vercel AI SDK, eve, Mastra, and Python packages, so a prompt written for one carries over.
The distinction worth internalising is extract versus generate. Extract pulls out what is already written on the page. Generate fetches the page and produces something derived from it, like a category or a summary nobody wrote. If the field exists in the HTML, extract it. If you are asking for judgement, generate it.
Step 3: the inputs the model fills in
You will read these in transcripts:
extract_structured_data:url,json_schema_json, a JSON encoded JSON Schema stringextract_page_content:urlresearch_question:querygenerate_structured_data:url,instructions,json_schema_jsonautomate_browser_task:task, plus optionalurl,guardrails,data,country,max_iterations,max_validation_attempts
Each tool's input schema is the core Zod schema's raw shape, which means the Claude Agent SDK validates Claude's arguments before the handler runs. Malformed tool calls fail immediately with a clear message rather than travelling to the API and coming back rejected.
There are optional inputs too, and each is accepted by a different subset of the tools:
effort, as"min","standard", or"max", on the three fetch-based toolsnocacheto bypass the cache, on the three fetch-based tools andresearch_questioncountry, an ISO 3166-1 alpha-2 code, for geotargeted fetches, on the three fetch-based tools andautomate_browser_taskmode, as"fast"or"balanced", onresearch_questiononly
The first three are forwarded only when Claude supplies them. mode is the exception: it defaults to "balanced" and is always sent.
If a page comes back thin or empty, it is nearly always a client-rendered app and effort: "max" is the fix. Full rendering happens server side, so there is no Playwright in your project and no browser binary in your image. Put it in your system prompt and let Claude apply it:
If a page returns little or no content, retry once with effort set to max. Always cite the sources you used.
Step 4: custom keys, and assembling it yourself
For a key per tenant, a custom base URL, or reuse of an SDK client you already have:
import { query } from "@anthropic-ai/claude-agent-sdk";
import {
createTabstackClaudeAgentServer,
tabstackAllowedTools,
tabstackMcpServerName,
} from "@tabstack/claude-agent";
const server = createTabstackClaudeAgentServer({ apiKey: process.env.MY_KEY });
for await (const message of query({
prompt: "...",
options: {
mcpServers: { [tabstackMcpServerName]: server },
allowedTools: tabstackAllowedTools,
},
})) {
if (message.type === "result" && message.subtype === "success") {
console.log(message.result);
}
}If you would rather assemble the server yourself, perhaps because you want Tabstack's tools alongside your own in a single server, createTabstackClaudeAgentTools(config) returns the raw tool array to pass into your own createSdkMcpServer({ name, version, tools }). That is also the route to take when you want a subset. Build the array, keep the tools you need, and hand the rest of your own tools in beside them.
Step 5: failures
This adapter handles errors differently from the others, and the difference is deliberate.
When a tool call fails, the handler normalises it to a TabstackToolError with a clean message, plus an HTTP status for API errors, then returns it as an MCP error result with isError: true. What Claude sees is the message; the status stays on the error object for your own logging rather than travelling into the transcript. Either way Claude reads a useful message rather than a raw exception, which means it can retry with different arguments or explain the problem to the user. A dead link does not end the run.
That is the right default for an agent loop. You are not catching these in your own code in the common case, because the model is the one that needs to react.
One behaviour to know before shipping: automate_browser_task runs non-interactively. It never pauses for human-in-the-loop form input, so it cannot block a run waiting on someone. It returns the final answer plus the data it extracted and the pages it visited. If you genuinely need a human in the middle of a browser session, that is the interactive mode flow in the API rather than this tool.
Where to take it next
Trim allowedTools. tabstackAllowedTools approves all five, which is the fastest way to get started and, I would argue, rarely what you want in production. Narrowing it to the two or three your agent actually needs reduces both cost and the chance of Claude choosing a tool you did not intend.
Combine it with your own tools. Because this is an in-process MCP server, adding Tabstack does not preclude anything else. Your own createSdkMcpServer output and tabstackServer sit side by side under different names in mcpServers.
The package is on npm as @tabstack/claude-agent, and the full API reference is at docs.tabstack.ai.
Ready to build? Sign up for 10,000 free credits, no card required, and hand your Claude agent its first live page. Full API in the docs.