Skip to content

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:

  • content for posts, pages, and custom post types
  • terms for categories, tags, and custom taxonomies
  • resources for media, comments, and users
  • abilities for WordPress ability endpoints
  • blocks for Gutenberg read/write workflows
  • settings for the singleton site settings endpoint
Installing dependencies…
npm install fluent-wp-client ai

ai is only required when importing from fluent-wp-client/ai-sdk.

tools.ts
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:

agent.ts
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.',
});
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.

You can either pin a tool to one resource in the factory options or let the model choose at runtime.

const searchPosts = getContentCollectionTool(wp, {
contentType: 'posts',
fixedArgs: { status: 'publish' },
});

The model does not see contentType in the tool input.

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.

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 / _fields on read tools narrow to discovered field paths (including nested acf.* and meta.* 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 contentType or taxonomyType
  • 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.

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(),
}),
}),
});
Use JSON Schema with AI SDK
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.

All generic tool factories accept the existing configuration model:

  • description — Override the AI-facing tool description
  • fixedArgs — Lock arguments that always override model-provided input
  • fixedInput — Lock nested input fields for mutation tools
  • strict — Enable provider strict-mode tool calling
  • needsApproval — Require approval before executing this tool
  • inputSchema — Override the generated schema with a custom AI SDK schema, such as Zod or jsonSchema()
  • fetch — Replace the default WordPress client call with a custom implementation
  • toolOptions — Pass additional AI SDK tool options such as title, inputExamples, lifecycle callbacks, outputSchema, or toModelOutput

Example:

const publishPosts = saveContentTool(adminWp, {
contentType: 'posts',
description: 'Publish a draft blog post by id.',
fixedInput: { status: 'publish' },
});

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 loaders
const searchPosts = getContentCollectionTool(wp, {
contentType: 'posts',
fetch: async ({ contentType, filter }) => {
return getLiveCollection(contentType, filter);
},
});
// Route a single-item read through a CDN cache
const 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 writing
const 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.

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.

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

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.

Use taxonomyType for categories, tags, and custom taxonomies.

const listTerms = getTermCollectionTool(wp);
const writeGenre = saveTermTool(adminWp, {
taxonomyType: 'genre',
});

Use resourceType for the first-class non-post-like resources:

  • media
  • comments
  • users
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.

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.

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.

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' }

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.

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.

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 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(),
});

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.

  • TypeScript92%
  • PHP6%
  • JavaScript2%