AI SDK Integration – fluent-wp-client
The fluent-wp-client/ai-sdk entrypoint provides generic tool factories for Vercel AI SDK. Instead of separate post, page, category, tag, and custom-type factories, the AI SDK surface is organized around resource families:
contentfor posts, pages, and custom post typestermsfor categories, tags, and custom taxonomiesresourcesfor media, comments, and usersabilitiesfor WordPress ability endpointsblocksfor Gutenberg read/write workflowssettingsfor the singleton site settings endpoint
Install
Section titled “Install”npm install fluent-wp-client aiai is only required when importing from fluent-wp-client/ai-sdk.
Quick start
Section titled “Quick start”import { WordPressClient } from 'fluent-wp-client';import { getContentCollectionTool, getContentTool, saveContentTool,} from 'fluent-wp-client/ai-sdk';
const wp = new WordPressClient({ baseUrl: 'https://example.com', auth: { username: 'admin', password: 'xxxx xxxx xxxx xxxx' },});
await wp.explore();
const tools = { searchPosts: getContentCollectionTool(wp, { contentType: 'posts', description: 'Search published blog posts.', fixedArgs: { status: 'publish', fields: ['id', 'slug', 'title', 'excerpt', 'link'] }, }), readContent: getContentTool(wp), writePost: saveContentTool(wp, { contentType: 'posts', fixedInput: { status: 'draft' }, }),};Pass these tools to generateText, streamText, or any AI SDK agent:
import { generateText } from 'ai';import { openai } from '@ai-sdk/openai';
const { text } = await generateText({ model: openai('gpt-4o'), tools, prompt: 'Find the latest 3 posts about TypeScript and summarize them.',});Generic tool families
Section titled “Generic tool families”import { // Content (posts, pages, custom post types) getContentCollectionTool, getContentTool, saveContentTool, deleteContentTool, // Terms (categories, tags, custom taxonomies) getTermCollectionTool, getTermTool, saveTermTool, deleteTermTool, // First-class resources (media, comments, users) getResourceCollectionTool, getResourceTool, saveResourceTool, deleteResourceTool, // Abilities getAbilitiesTool, getAbilityTool, executeGetAbilityTool, executeRunAbilityTool, executeDeleteAbilityTool, createAbilityTools, // Gutenberg blocks getBlocksTool, setBlocksTool, // Settings getSettingsTool, updateSettingsTool, // Schema introspection describeResourceTool,} from 'fluent-wp-client/ai-sdk';Each resource family exposes a single save*Tool that routes to create or update based on whether the model provides an id — this matches the WordPress REST API, which uses POST for both operations.
Fixed vs dynamic selectors
Section titled “Fixed vs dynamic selectors”You can either pin a tool to one resource in the factory options or let the model choose at runtime.
Fixed resource
Section titled “Fixed resource”const searchPosts = getContentCollectionTool(wp, { contentType: 'posts', fixedArgs: { status: 'publish' },});The model does not see contentType in the tool input.
Dynamic resource
Section titled “Dynamic resource”const searchAnyContent = getContentCollectionTool(wp);
// Model input// { contentType: 'books', search: 'API design' }When the client has a cached catalog, contentType, taxonomyType, resourceType, and ability name become enums in the tool schema.
Catalog-aware schemas
Section titled “Catalog-aware schemas”Populate the client cache with wp.explore() or wp.useCatalog() when you want the tool schema to reflect the connected WordPress instance.
const catalog = await wp.explore();wp.useCatalog(catalog);
const writeBook = saveContentTool(wp, { contentType: 'books',});With a cached catalog on the client:
- selector fields become enums when they are model-facing
fields/_fieldson read tools narrow to discovered field paths (including nestedacf.*andmeta.*entries) so bad field names are rejected locally- fixed-resource content and term save tools use the live discovered update schema (or create schema as fallback) so one payload shape covers both modes
- dynamic content and term save tools become discriminated unions keyed by
contentTypeortaxonomyType - ability execution tools can expose real ability-name enums and discovered input schemas
Field meaning (descriptions, choice labels, plugin-specific keywords) is intentionally not bundled into the tool schemas. Use describeResourceTool whenever the model needs to understand what a field is for — it returns the raw discovery DTO, including any plugin-added keywords such as ACF’s description or the custom choices keyword documented in Plugin support → ACF.
This is especially useful for custom post types, ACF fields, plugin taxonomies, and plugin-added ability endpoints.
Manual schemas without discovery
Section titled “Manual schemas without discovery”If you do not want to run discovery, pass inputSchema directly to the tool factory. AI SDK supports both Zod schemas and JSON Schema wrappers created with jsonSchema().
import { z } from 'zod';
const writeBook = saveContentTool(wp, { contentType: 'books', inputSchema: z.object({ id: z.number().int().optional(), input: z.object({ title: z.string(), isbn: z.string(), publishNow: z.boolean().optional(), }), }),});import { jsonSchema } from 'ai';
const schema = await wp.content('books').getJsonSchema('update');
const writeBook = saveContentTool(wp, { contentType: 'books', inputSchema: jsonSchema({ type: 'object', properties: { id: { type: 'integer' }, input: schema, }, required: ['input'], }),});This overrides the generated AI-facing schema without changing tool execution. Keep id optional in custom schemas so the save tool can still distinguish create from update.
Configuration options
Section titled “Configuration options”All generic tool factories accept the existing configuration model:
description— Override the AI-facing tool descriptionfixedArgs— Lock arguments that always override model-provided inputfixedInput— Lock nested input fields for mutation toolsstrict— Enable provider strict-mode tool callingneedsApproval— Require approval before executing this toolinputSchema— Override the generated schema with a custom AI SDK schema, such as Zod orjsonSchema()fetch— Replace the default WordPress client call with a custom implementationtoolOptions— Pass additional AI SDK tool options such astitle,inputExamples, lifecycle callbacks,outputSchema, ortoModelOutput
Example:
const publishPosts = saveContentTool(adminWp, { contentType: 'posts', description: 'Publish a draft blog post by id.', fixedInput: { status: 'publish' },});Custom fetch callbacks
Section titled “Custom fetch callbacks”Every tool factory accepts an optional fetch callback that replaces the default WordPress client call. The callback receives the fully resolved, normalised arguments after fixedArgs and schema defaults have been applied — the tool’s own argument merging always runs first.
This works for reads and writes, making it the single extension point for caches, live loaders, proxies, audit logs, or any custom request pipeline.
// Route a read through Astro live loadersconst searchPosts = getContentCollectionTool(wp, { contentType: 'posts', fetch: async ({ contentType, filter }) => { return getLiveCollection(contentType, filter); },});
// Route a single-item read through a CDN cacheconst getPost = getContentTool(wp, { contentType: 'posts', fetch: async ({ contentType, slug, includeContent }) => { if (!slug) throw new Error('slug required'); return myEdgeCache.get(contentType, slug, { includeContent }); },});
// Route save mutations through an audit log before writingconst savePost = saveContentTool(wp, { contentType: 'posts', fetch: async ({ contentType, id, input }) => { await auditLog.record(id === undefined ? 'create' : 'update', contentType, input); const resource = wp.content(contentType); return id === undefined ? resource.create(input as never) : resource.update(id, input as never); },});The fetch callback is typed specifically to the tool it is passed to, so TypeScript knows exactly which arguments are available. For collection tools the input is { contentType, filter } or { taxonomyType, filter }. For single-item reads it adds id? and slug?. For save mutations it carries an optional id plus input.
AI SDK tool options
Section titled “AI SDK tool options”Use toolOptions when you need AI SDK-native tool configuration that is not WordPress-specific. This keeps the WordPress factory options separate from the raw tool() options while preserving this package’s generated execution, schema, approval, and strict-mode behavior.
const searchResources = getContentCollectionTool(wp, { contentType: 'resources', fixedArgs: { perPage: 5, status: 'publish' }, toolOptions: { title: 'Search resources', inputExamples: [{ input: { search: 'accessibility checklist' } }], toModelOutput: ({ output }) => ({ type: 'content', value: [ { type: 'text', text: Array.isArray(output) ? output.map((item) => String(item.title?.rendered ?? item.slug ?? item.id)).join('\n') : JSON.stringify(output), }, ], }), },});toolOptions intentionally excludes execute, inputSchema, description, needsApproval, strict, and provider-defined tool mode fields. Use the top-level factory options for those controlled fields so fixed arguments, catalog-aware schemas, error handling, and WordPress routing still run consistently.
Default values with Zod schemas
Section titled “Default values with Zod schemas”Instead of defaultArgs, use Zod’s .default() method in a custom inputSchema:
import { z } from 'zod';
const searchPosts = getContentCollectionTool(wp, { contentType: 'posts', inputSchema: z.object({ search: z.string().optional(), perPage: z.number().default(5), page: z.number().default(1), }),});This approach:
- Leverages Zod’s built-in default handling during validation
- Works with catalog-discovered schemas extended via
.extend()and.default() - Is fully type-safe and self-documenting
Content tools
Section titled “Content tools”Use contentType for posts, pages, and custom post types.
const tools = { searchContent: getContentCollectionTool(wp), readContent: getContentTool(wp), writeContent: saveContentTool(adminWp),};Model-facing examples:
{ contentType: 'posts', search: 'release notes', perPage: 5 }{ contentType: 'pages', slug: 'about' }
// Create a new book — no id.{ contentType: 'books', input: { title: 'New Book', status: 'draft' } }
// Update an existing book — id is present.{ contentType: 'books', id: 42, input: { status: 'publish' } }Single-item content reads return a content envelope:
{ item: { id: 42, slug: 'hello-world', ... }, content: { raw: '<!-- wp:paragraph -->...', rendered: '<p>...</p>', protected: false }, blocks: [{ blockName: 'core/paragraph', ... }]}content and blocks are included when includeContent or includeBlocks is enabled.
Term tools
Section titled “Term tools”Use taxonomyType for categories, tags, and custom taxonomies.
const listTerms = getTermCollectionTool(wp);const writeGenre = saveTermTool(adminWp, { taxonomyType: 'genre',});Resource tools
Section titled “Resource tools”Use resourceType for the first-class non-post-like resources:
mediacommentsusers
const listUsers = getResourceCollectionTool(adminWp, { resourceType: 'users',});
const writeUser = saveResourceTool(adminWp, { resourceType: 'users',});
const deleteComment = deleteResourceTool(adminWp, { resourceType: 'comments',});saveResourceTool supports comments and users. It creates when id is absent and updates when id is provided.
Media writes are not wrapped because uploads require file transport details. Use client.media().upload() directly when you need uploads.
Abilities
Section titled “Abilities”Ability tools can stay dynamic or be pinned to one ability.
const runAbility = executeRunAbilityTool(adminWp);
const runCalculator = executeRunAbilityTool(adminWp, { abilityName: 'acme/calculate',});With a cached catalog, the generic execution tools can expose real ability-name enums and the discovered input schema for each ability.
Generating one tool per ability
Section titled “Generating one tool per ability”createAbilityTools reads the cached catalog and returns a record of AI SDK tools, one per discovered ability. The tool key is the ability name with / replaced by _.
import { createAbilityTools } from 'fluent-wp-client/ai-sdk';
const catalog = await adminWp.explore();adminWp.useCatalog(catalog);
const abilityTools = createAbilityTools(adminWp, { // Only expose safe read abilities to the model include: ['acme/get-stats', 'acme/search-products'], needsApproval: (name, ability) => ability.annotations?.destructive === true,});
// { acme_get-stats: Tool, acme_search-products: Tool }Pass exclude to remove specific abilities, or omit both to expose all discovered abilities.
Schema introspection
Section titled “Schema introspection”describeResourceTool gives the model one generic way to introspect the live WordPress schema for any discoverable resource or ability. It does not summarize, remap, or reshape the schema — it returns the raw WordPressResourceDescription or WordPressAbilityDescription straight from the catalog, including any plugin-added keywords such as ACF description or the custom choices keyword documented in Plugin support → ACF.
Use it as the canonical way to teach the model about unfamiliar resources before it composes calls to other tools. With a cached catalog, repeated describe calls resolve from memory.
import { describeResourceTool } from 'fluent-wp-client/ai-sdk';
const tools = { describeResource: describeResourceTool(wp),};Model-facing inputs:
{ kind: 'content', name: 'books' }{ kind: 'term', name: 'categories' }{ kind: 'resource', name: 'users' }{ kind: 'ability', name: 'acme/send-email' }Limiting the exposed surface
Section titled “Limiting the exposed surface”Pass include to constrain which kinds and names the model can request. Any kind that is omitted from include is removed from the tool schema entirely — the Zod enum will not even contain its discriminator, and runtime requests against that kind return a structured error envelope.
const describe = describeResourceTool(wp, { include: { content: ['posts', 'books'], term: ['categories', 'genre'], // `resource` and `ability` omitted → not exposed to the model },});An explicit empty array also disables the kind, which is useful if you want to keep the field in the config for clarity without letting the model use it.
When no catalog is available and include is not provided, kind remains unconstrained — helpful for agents that talk to arbitrary WordPress installs without running discovery first.
Error envelope
Section titled “Error envelope”Runtime validation failures (for example, a name that slips past Zod because include is enforced only at execution time, or a kind that was disabled via include) return the same structured envelope the rest of the AI SDK tools use instead of throwing:
{ ok: false, error: { kind: 'invalid_request', message: "describeResourceTool(): 'posts' is not available for kind 'content'. Allowed values: books.", },}HTTP-level failures from the underlying describe call (network errors, auth issues, unknown resource types) are surfaced through the same envelope, with the originating error’s kind, status, and statusText carried through when available.
Gutenberg blocks
Section titled “Gutenberg blocks”Block tools use the same contentType selector model as content tools. Both require edit-level authentication.
const getBlocks = getBlocksTool(adminWp, { contentType: 'posts',});
const setBlocks = setBlocksTool(adminWp, { contentType: 'posts',});When the tool is not pinned, the model supplies contentType at runtime.
getBlocksTool also accepts a fetch callback to replace the default client read — useful when block content is already available from a cache:
const getBlocks = getBlocksTool(adminWp, { contentType: 'posts', fetch: async ({ contentType, id }) => { return blockCache.get(contentType, id); },});Settings
Section titled “Settings”Settings remain a singleton endpoint:
const tools = { getSettings: getSettingsTool(adminWp), updateSettings: updateSettingsTool(adminWp),};getSettingsTool accepts a fetch callback for cached reads. Because settings are a singleton the callback receives no arguments:
const getSettings = getSettingsTool(adminWp, { fetch: async () => settingsCache.get(),});Field selection
Section titled “Field selection”Collection tools and single-item tools support fields to reduce token usage:
const searchPosts = getContentCollectionTool(wp, { contentType: 'posts', fixedArgs: { fields: ['id', 'slug', 'title', 'excerpt', 'link'] },});Both fields and _fields are accepted.