From fca943d4f72826405fad3ba52b347fa7d71d3f56 Mon Sep 17 00:00:00 2001 From: Jack Arturo Date: Thu, 11 Jun 2026 01:01:42 +0200 Subject: [PATCH] Fix taxonomy tools for taxonomies whose rest_base differs from slug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every taxonomy code path assumed a custom taxonomy's REST field name and endpoint equal its slug. WordPress only guarantees that for built-ins; any register_taxonomy() call can set a different rest_base (e.g. slug documentation_category, rest_base documentation-categories), and for those taxonomies the tools were broken three ways: - assign_terms_to_content sent the slug as the post field name. WordPress ignores unknown fields and returns 200, so the tool reported success while writing nothing. - get_content_terms read post fields by slug and returned {} even when terms existed. - getTaxonomyEndpoint() fell back to the slug, 404ing list/get/create/ update/delete_term for any taxonomy with a divergent rest_base. Fixes, per the resolver pattern: - New resolveTaxonomy(input, siteId) resolves slug OR rest_base through a per-site cached /wp/v2/taxonomies lookup, hard-erroring on unknown taxonomies (the silent slug fallback is what masked the no-op writes). All term CRUD and field lookups route through it. - assign_terms_to_content now writes to the rest_base field (collapsing the category/post_tag special cases) and derives success from the response: requested term IDs must appear in the updated content's rest_base field, otherwise the tool errors. terms is now number[] — string slugs were never accepted by the WordPress REST post fields. - get_content_terms reads content[rest_base] in both the single-taxonomy and all-taxonomies paths and fetches term details from the rest_base endpoint. - The local sync getContentEndpoint() copy (same slug-fallback bug for post types) is gone; reuses the async site-aware resolver exported from unified-content.ts. - All 8 taxonomy tools accept site_id now, matching the rest of the server (docs previously claimed this; the taxonomies cache is keyed per site). Found during a live run against a production site; see repro in the tool descriptions and README notes. Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 9 + README.md | 7 + src/tools/unified-content.ts | 3 +- src/tools/unified-taxonomies.ts | 430 +++++++++++++++++--------------- 4 files changed, 248 insertions(+), 201 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e6a0d55..6a93a69 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -216,6 +216,15 @@ All taxonomy operations use a single `taxonomy` parameter: } ``` +The `taxonomy` parameter accepts either the taxonomy slug or its `rest_base` +(custom taxonomies can register a rest_base that differs from the slug, e.g. +slug `documentation_category` with rest_base `documentation-categories`). +All taxonomy tools resolve the identifier through a per-site cached +`/wp/v2/taxonomies` lookup and hard-error on unknown taxonomies — there is no +slug fallback. `assign_terms_to_content` derives success from the WordPress +response (the updated content's `rest_base` field must contain the requested +term IDs) rather than echoing the request. + #### Multi-Site Support All tools accept an optional `site_id` parameter to target specific sites: ```json diff --git a/README.md b/README.md index 1cdb6b6..7943784 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,13 @@ All taxonomy operations use a single `taxonomy` parameter: } ``` +The `taxonomy` parameter accepts either the taxonomy slug or its `rest_base` +(they can differ for custom taxonomies, e.g. slug `documentation_category` +with rest_base `documentation-categories`). Tools resolve the identifier via +`/wp/v2/taxonomies` and error on unknown taxonomies instead of guessing. +`assign_terms_to_content` verifies the write against the WordPress response +and reports an error if the terms were not actually saved. + ## Configuration ### Single Site Configuration diff --git a/src/tools/unified-content.ts b/src/tools/unified-content.ts index 1e0f92f..1de19d8 100644 --- a/src/tools/unified-content.ts +++ b/src/tools/unified-content.ts @@ -93,7 +93,8 @@ async function getPostTypes(forceRefresh = false, siteId?: string) { } // Helper function to get the correct endpoint for a content type -async function getContentEndpoint(contentType: string, siteId?: string): Promise { +// Exported for reuse by unified-taxonomies.ts (assign_terms_to_content / get_content_terms) +export async function getContentEndpoint(contentType: string, siteId?: string): Promise { // Quick return for standard types const standardMap: Record = { 'post': 'posts', diff --git a/src/tools/unified-taxonomies.ts b/src/tools/unified-taxonomies.ts index 4128101..e0ea63f 100644 --- a/src/tools/unified-taxonomies.ts +++ b/src/tools/unified-taxonomies.ts @@ -1,27 +1,32 @@ // src/tools/unified-taxonomies.ts import { Tool } from '@modelcontextprotocol/sdk/types.js'; import { makeWordPressRequest, logToFile } from '../wordpress.js'; +import { getContentEndpoint } from './unified-content.js'; import { z } from 'zod'; -// Cache for taxonomies to reduce API calls -let taxonomiesCache: any = null; -let taxonomyCacheTimestamp: number = 0; +// Cache for taxonomies, keyed per site, to reduce API calls +interface TaxonomyCacheEntry { + data: any; + timestamp: number; +} +const taxonomiesCache = new Map(); const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes -// Helper function to get all taxonomies with caching -async function getTaxonomies(forceRefresh = false) { +// Helper function to get all taxonomies for a site with caching +async function getTaxonomies(forceRefresh = false, siteId?: string) { + const cacheKey = siteId || '__default__'; const now = Date.now(); - - if (!forceRefresh && taxonomiesCache && (now - taxonomyCacheTimestamp) < CACHE_DURATION) { + const cached = taxonomiesCache.get(cacheKey); + + if (!forceRefresh && cached && (now - cached.timestamp) < CACHE_DURATION) { logToFile('Using cached taxonomies'); - return taxonomiesCache; + return cached.data; } try { logToFile('Fetching taxonomies from API'); - const response = await makeWordPressRequest('GET', 'taxonomies'); - taxonomiesCache = response; - taxonomyCacheTimestamp = now; + const response = await makeWordPressRequest('GET', 'taxonomies', undefined, { siteId }); + taxonomiesCache.set(cacheKey, { data: response, timestamp: now }); return response; } catch (error: any) { logToFile(`Error fetching taxonomies: ${error.message}`); @@ -29,36 +34,54 @@ async function getTaxonomies(forceRefresh = false) { } } -// Helper function to get the correct endpoint for a taxonomy -function getTaxonomyEndpoint(taxonomy: string): string { - const endpointMap: Record = { - 'category': 'categories', - 'post_tag': 'tags', - 'nav_menu': 'menus', - 'link_category': 'link_categories' +/** + * Resolve a taxonomy identifier (slug or rest_base) to its canonical slug and + * rest_base via the site's /wp/v2/taxonomies response. + * + * WordPress only guarantees rest_base === slug for built-ins; any + * register_taxonomy() call can set a different rest_base (e.g. slug + * "documentation_category" with rest_base "documentation-categories"). + * The REST field name on post objects and the term endpoint both use + * rest_base, so every code path must route through this resolver. + * + * Hard-errors on unknown taxonomies: a silent slug fallback is what + * previously let writes report success while writing nothing. + */ +async function resolveTaxonomy(input: string, siteId?: string): Promise<{ slug: string; restBase: string }> { + const findIn = (taxonomies: any) => { + for (const [slug, info] of Object.entries(taxonomies)) { + if (slug === input || info.rest_base === input) { + return { slug, restBase: info.rest_base || slug }; + } + } + return null; }; - - return endpointMap[taxonomy] || taxonomy; -} -// Helper function to get the correct content endpoint -function getContentEndpoint(contentType: string): string { - const endpointMap: Record = { - 'post': 'posts', - 'page': 'pages' - }; - - return endpointMap[contentType] || contentType; + let match = findIn(await getTaxonomies(false, siteId)); + if (match) return match; + + // Refresh once in case the taxonomy was registered after the cache was filled + const fresh = await getTaxonomies(true, siteId); + match = findIn(fresh); + if (match) return match; + + const available = Object.entries(fresh) + .map(([slug, info]) => (info.rest_base && info.rest_base !== slug ? `${slug} (rest_base: ${info.rest_base})` : slug)) + .join(', '); + throw new Error(`Unknown taxonomy "${input}" — not found in /wp/v2/taxonomies. Available: ${available}`); } // Schema definitions +const siteIdSchema = z.string().optional().describe("Site ID (for multi-site setups). Uses the default site if omitted."); + const discoverTaxonomiesSchema = z.object({ content_type: z.string().optional().describe("Limit results to taxonomies associated with a specific content type"), - refresh_cache: z.boolean().optional().describe("Force refresh the taxonomies cache") + refresh_cache: z.boolean().optional().describe("Force refresh the taxonomies cache"), + site_id: siteIdSchema }); const listTermsSchema = z.object({ - taxonomy: z.string().describe("The taxonomy slug (e.g., 'category', 'post_tag', or custom taxonomies)"), + taxonomy: z.string().describe("The taxonomy slug or rest_base (e.g., 'category', 'post_tag', or custom taxonomies)"), page: z.number().optional().describe("Page number (default 1)"), per_page: z.number().min(1).max(100).optional().describe("Items per page (default 10, max 100)"), search: z.string().optional().describe("Search term for term name"), @@ -66,51 +89,58 @@ const listTermsSchema = z.object({ slug: z.string().optional().describe("Limit result to terms with a specific slug"), hide_empty: z.boolean().optional().describe("Whether to hide terms not assigned to any content"), orderby: z.enum(['id', 'include', 'name', 'slug', 'term_group', 'description', 'count']).optional().describe("Sort terms by parameter"), - order: z.enum(['asc', 'desc']).optional().describe("Order sort attribute") + order: z.enum(['asc', 'desc']).optional().describe("Order sort attribute"), + site_id: siteIdSchema }); const getTermSchema = z.object({ - taxonomy: z.string().describe("The taxonomy slug"), - id: z.number().describe("Term ID") + taxonomy: z.string().describe("The taxonomy slug or rest_base"), + id: z.number().describe("Term ID"), + site_id: siteIdSchema }); const createTermSchema = z.object({ - taxonomy: z.string().describe("The taxonomy slug"), + taxonomy: z.string().describe("The taxonomy slug or rest_base"), name: z.string().describe("Term name"), slug: z.string().optional().describe("Term slug"), parent: z.number().optional().describe("Parent term ID"), description: z.string().optional().describe("Term description"), - meta: z.record(z.any()).optional().describe("Term meta fields") + meta: z.record(z.any()).optional().describe("Term meta fields"), + site_id: siteIdSchema }); const updateTermSchema = z.object({ - taxonomy: z.string().describe("The taxonomy slug"), + taxonomy: z.string().describe("The taxonomy slug or rest_base"), id: z.number().describe("Term ID"), name: z.string().optional().describe("Term name"), slug: z.string().optional().describe("Term slug"), parent: z.number().optional().describe("Parent term ID"), description: z.string().optional().describe("Term description"), - meta: z.record(z.any()).optional().describe("Term meta fields") + meta: z.record(z.any()).optional().describe("Term meta fields"), + site_id: siteIdSchema }); const deleteTermSchema = z.object({ - taxonomy: z.string().describe("The taxonomy slug"), + taxonomy: z.string().describe("The taxonomy slug or rest_base"), id: z.number().describe("Term ID"), - force: z.boolean().optional().describe("Required to be true, as terms do not support trashing") + force: z.boolean().optional().describe("Required to be true, as terms do not support trashing"), + site_id: siteIdSchema }); const assignTermsToContentSchema = z.object({ content_id: z.number().describe("The content ID"), content_type: z.string().describe("The content type slug"), - taxonomy: z.string().describe("The taxonomy slug"), - terms: z.array(z.union([z.number(), z.string()])).describe("Array of term IDs or slugs to assign"), - append: z.boolean().optional().describe("If true, append terms to existing ones. If false, replace all terms") + taxonomy: z.string().describe("The taxonomy slug or rest_base"), + terms: z.array(z.number()).describe("Array of term IDs to assign"), + append: z.boolean().optional().describe("If true, append terms to existing ones. If false, replace all terms"), + site_id: siteIdSchema }); const getContentTermsSchema = z.object({ content_id: z.number().describe("The content ID"), content_type: z.string().describe("The content type slug"), - taxonomy: z.string().optional().describe("Specific taxonomy to retrieve terms from (if not specified, returns all)") + taxonomy: z.string().optional().describe("Specific taxonomy (slug or rest_base) to retrieve terms from (if not specified, returns all)"), + site_id: siteIdSchema }); // Type definitions @@ -126,7 +156,7 @@ type GetContentTermsParams = z.infer; export const unifiedTaxonomyTools: Tool[] = [ { name: "discover_taxonomies", - description: "Discovers all available taxonomies (built-in and custom) in the WordPress site", + description: "Discovers all available taxonomies (built-in and custom) in the WordPress site, including each taxonomy's rest_base", inputSchema: { type: "object", properties: discoverTaxonomiesSchema.shape } }, { @@ -156,7 +186,7 @@ export const unifiedTaxonomyTools: Tool[] = [ }, { name: "assign_terms_to_content", - description: "Assigns taxonomy terms to content of any type", + description: "Assigns taxonomy terms to content of any type. Verifies the write against the WordPress response and errors if the terms were not actually saved.", inputSchema: { type: "object", properties: assignTermsToContentSchema.shape } }, { @@ -169,18 +199,18 @@ export const unifiedTaxonomyTools: Tool[] = [ export const unifiedTaxonomyHandlers = { discover_taxonomies: async (params: DiscoverTaxonomiesParams) => { try { - const taxonomies = await getTaxonomies(params.refresh_cache || false); - + const taxonomies = await getTaxonomies(params.refresh_cache || false, params.site_id); + // Filter by content type if specified let filteredTaxonomies = taxonomies; if (params.content_type) { filteredTaxonomies = Object.fromEntries( - Object.entries(taxonomies).filter(([_, tax]: [string, any]) => + Object.entries(taxonomies).filter(([_, tax]: [string, any]) => tax.types && tax.types.includes(params.content_type) ) ); } - + // Format the response to be more readable const formattedTaxonomies = Object.entries(filteredTaxonomies).map(([slug, tax]: [string, any]) => ({ slug, @@ -191,12 +221,12 @@ export const unifiedTaxonomyHandlers = { rest_base: tax.rest_base, labels: tax.labels })); - + return { toolResult: { - content: [{ - type: 'text', - text: JSON.stringify(formattedTaxonomies, null, 2) + content: [{ + type: 'text', + text: JSON.stringify(formattedTaxonomies, null, 2) }], isError: false } @@ -204,9 +234,9 @@ export const unifiedTaxonomyHandlers = { } catch (error: any) { return { toolResult: { - content: [{ - type: 'text', - text: `Error discovering taxonomies: ${error.message}` + content: [{ + type: 'text', + text: `Error discovering taxonomies: ${error.message}` }], isError: true } @@ -216,16 +246,16 @@ export const unifiedTaxonomyHandlers = { list_terms: async (params: ListTermsParams) => { try { - const endpoint = getTaxonomyEndpoint(params.taxonomy); - const { taxonomy, ...queryParams } = params; - - const response = await makeWordPressRequest('GET', endpoint, queryParams); - + const { restBase } = await resolveTaxonomy(params.taxonomy, params.site_id); + const { taxonomy, site_id, ...queryParams } = params; + + const response = await makeWordPressRequest('GET', restBase, queryParams, { siteId: site_id }); + return { toolResult: { - content: [{ - type: 'text', - text: JSON.stringify(response, null, 2) + content: [{ + type: 'text', + text: JSON.stringify(response, null, 2) }], isError: false } @@ -233,9 +263,9 @@ export const unifiedTaxonomyHandlers = { } catch (error: any) { return { toolResult: { - content: [{ - type: 'text', - text: `Error listing terms: ${error.message}` + content: [{ + type: 'text', + text: `Error listing terms: ${error.message}` }], isError: true } @@ -245,15 +275,15 @@ export const unifiedTaxonomyHandlers = { get_term: async (params: GetTermParams) => { try { - const endpoint = getTaxonomyEndpoint(params.taxonomy); - - const response = await makeWordPressRequest('GET', `${endpoint}/${params.id}`); - + const { restBase } = await resolveTaxonomy(params.taxonomy, params.site_id); + + const response = await makeWordPressRequest('GET', `${restBase}/${params.id}`, undefined, { siteId: params.site_id }); + return { toolResult: { - content: [{ - type: 'text', - text: JSON.stringify(response, null, 2) + content: [{ + type: 'text', + text: JSON.stringify(response, null, 2) }], isError: false } @@ -261,9 +291,9 @@ export const unifiedTaxonomyHandlers = { } catch (error: any) { return { toolResult: { - content: [{ - type: 'text', - text: `Error getting term: ${error.message}` + content: [{ + type: 'text', + text: `Error getting term: ${error.message}` }], isError: true } @@ -273,24 +303,24 @@ export const unifiedTaxonomyHandlers = { create_term: async (params: CreateTermParams) => { try { - const endpoint = getTaxonomyEndpoint(params.taxonomy); - + const { restBase } = await resolveTaxonomy(params.taxonomy, params.site_id); + const termData: any = { name: params.name }; - + if (params.slug !== undefined) termData.slug = params.slug; if (params.parent !== undefined) termData.parent = params.parent; if (params.description !== undefined) termData.description = params.description; if (params.meta !== undefined) termData.meta = params.meta; - - const response = await makeWordPressRequest('POST', endpoint, termData); - + + const response = await makeWordPressRequest('POST', restBase, termData, { siteId: params.site_id }); + return { toolResult: { - content: [{ - type: 'text', - text: JSON.stringify(response, null, 2) + content: [{ + type: 'text', + text: JSON.stringify(response, null, 2) }], isError: false } @@ -298,9 +328,9 @@ export const unifiedTaxonomyHandlers = { } catch (error: any) { return { toolResult: { - content: [{ - type: 'text', - text: `Error creating term: ${error.message}` + content: [{ + type: 'text', + text: `Error creating term: ${error.message}` }], isError: true } @@ -310,23 +340,23 @@ export const unifiedTaxonomyHandlers = { update_term: async (params: UpdateTermParams) => { try { - const endpoint = getTaxonomyEndpoint(params.taxonomy); - + const { restBase } = await resolveTaxonomy(params.taxonomy, params.site_id); + const updateData: any = {}; - + if (params.name !== undefined) updateData.name = params.name; if (params.slug !== undefined) updateData.slug = params.slug; if (params.parent !== undefined) updateData.parent = params.parent; if (params.description !== undefined) updateData.description = params.description; if (params.meta !== undefined) updateData.meta = params.meta; - - const response = await makeWordPressRequest('POST', `${endpoint}/${params.id}`, updateData); - + + const response = await makeWordPressRequest('POST', `${restBase}/${params.id}`, updateData, { siteId: params.site_id }); + return { toolResult: { - content: [{ - type: 'text', - text: JSON.stringify(response, null, 2) + content: [{ + type: 'text', + text: JSON.stringify(response, null, 2) }], isError: false } @@ -334,9 +364,9 @@ export const unifiedTaxonomyHandlers = { } catch (error: any) { return { toolResult: { - content: [{ - type: 'text', - text: `Error updating term: ${error.message}` + content: [{ + type: 'text', + text: `Error updating term: ${error.message}` }], isError: true } @@ -346,17 +376,17 @@ export const unifiedTaxonomyHandlers = { delete_term: async (params: DeleteTermParams) => { try { - const endpoint = getTaxonomyEndpoint(params.taxonomy); - - const response = await makeWordPressRequest('DELETE', `${endpoint}/${params.id}`, { + const { restBase } = await resolveTaxonomy(params.taxonomy, params.site_id); + + const response = await makeWordPressRequest('DELETE', `${restBase}/${params.id}`, { force: true // Terms require force to be true - }); - + }, { siteId: params.site_id }); + return { toolResult: { - content: [{ - type: 'text', - text: JSON.stringify(response, null, 2) + content: [{ + type: 'text', + text: JSON.stringify(response, null, 2) }], isError: false } @@ -364,9 +394,9 @@ export const unifiedTaxonomyHandlers = { } catch (error: any) { return { toolResult: { - content: [{ - type: 'text', - text: `Error deleting term: ${error.message}` + content: [{ + type: 'text', + text: `Error deleting term: ${error.message}` }], isError: true } @@ -376,56 +406,68 @@ export const unifiedTaxonomyHandlers = { assign_terms_to_content: async (params: AssignTermsToContentParams) => { try { - // Determine the content endpoint - const contentEndpoint = getContentEndpoint(params.content_type); - - // Prepare the update data - const updateData: any = {}; - - // The field name depends on the taxonomy - if (params.taxonomy === 'category') { - updateData.categories = params.terms; - } else if (params.taxonomy === 'post_tag') { - updateData.tags = params.terms; - } else { - // For custom taxonomies, use the taxonomy slug as the field name - updateData[params.taxonomy] = params.terms; - } - - // If appending, we need to get current terms first + // The REST field on a post object is the taxonomy's rest_base, not its + // slug (for built-ins they coincide: categories/tags). Sending an + // unknown field is silently ignored by WordPress, so resolving here is + // what makes the write actually land. + const { slug, restBase } = await resolveTaxonomy(params.taxonomy, params.site_id); + const contentEndpoint = await getContentEndpoint(params.content_type, params.site_id); + + let termsToAssign = [...params.terms]; + + // If appending, merge with the content's current terms if (params.append) { try { - const currentContent = await makeWordPressRequest('GET', `${contentEndpoint}/${params.content_id}`); - const currentTerms = currentContent[params.taxonomy === 'category' ? 'categories' : - params.taxonomy === 'post_tag' ? 'tags' : - params.taxonomy] || []; - - // Merge current terms with new terms (remove duplicates) - const allTerms = [...new Set([...currentTerms, ...params.terms])]; - updateData[params.taxonomy === 'category' ? 'categories' : - params.taxonomy === 'post_tag' ? 'tags' : - params.taxonomy] = allTerms; + const currentContent = await makeWordPressRequest('GET', `${contentEndpoint}/${params.content_id}`, undefined, { siteId: params.site_id }); + const currentTerms: number[] = currentContent[restBase] || []; + termsToAssign = [...new Set([...currentTerms, ...params.terms])]; } catch (error) { // If we can't get current terms, just set the new ones logToFile(`Warning: Could not get current terms for append operation: ${error}`); } } - - const response = await makeWordPressRequest('POST', `${contentEndpoint}/${params.content_id}`, updateData); - + + const updateData = { [restBase]: termsToAssign }; + const response = await makeWordPressRequest('POST', `${contentEndpoint}/${params.content_id}`, updateData, { siteId: params.site_id }); + + // Derive success from the response, not the request: WordPress returns + // 200 even when it ignored the field entirely. + const savedTerms: number[] | undefined = Array.isArray(response[restBase]) ? response[restBase] : undefined; + const missing = savedTerms === undefined + ? params.terms + : params.terms.filter(id => !savedTerms.includes(id)); + + if (missing.length > 0) { + return { + toolResult: { + content: [{ + type: 'text', + text: `Error: assignment did not stick. Requested term(s) [${params.terms.join(', ')}] for taxonomy "${slug}" (field "${restBase}"), but the updated content reports ${savedTerms === undefined ? `no "${restBase}" field — the taxonomy may not be registered for content type "${params.content_type}" or not exposed in REST` : `[${savedTerms.join(', ')}]`}. Nothing was reported as saved for: [${missing.join(', ')}].` + }], + isError: true + } + }; + } + return { toolResult: { - content: [{ - type: 'text', + content: [{ + type: 'text', text: JSON.stringify({ success: true, + verified: true, content_id: params.content_id, content_type: params.content_type, - taxonomy: params.taxonomy, - assigned_terms: params.terms, + taxonomy: slug, + rest_base: restBase, + assigned_terms: savedTerms, appended: params.append || false, - content: response - }, null, 2) + content: { + id: response.id, + link: response.link, + [restBase]: savedTerms + } + }, null, 2) }], isError: false } @@ -433,9 +475,9 @@ export const unifiedTaxonomyHandlers = { } catch (error: any) { return { toolResult: { - content: [{ - type: 'text', - text: `Error assigning terms to content: ${error.message}` + content: [{ + type: 'text', + text: `Error assigning terms to content: ${error.message}` }], isError: true } @@ -446,70 +488,58 @@ export const unifiedTaxonomyHandlers = { get_content_terms: async (params: GetContentTermsParams) => { try { // First, get the content to see what taxonomies are assigned - const contentEndpoint = getContentEndpoint(params.content_type); - const content = await makeWordPressRequest('GET', `${contentEndpoint}/${params.content_id}`); - - // Get all available taxonomies - const taxonomies = await getTaxonomies(); - + const contentEndpoint = await getContentEndpoint(params.content_type, params.site_id); + const content = await makeWordPressRequest('GET', `${contentEndpoint}/${params.content_id}`, undefined, { siteId: params.site_id }); + const terms: any = {}; - + + // Fetch full term details for a list of term IDs from a taxonomy endpoint + const fetchTermDetails = (restBase: string, termIds: number[]) => Promise.all( + termIds.map(async (termId: number) => { + try { + return await makeWordPressRequest('GET', `${restBase}/${termId}`, undefined, { siteId: params.site_id }); + } catch { + return { id: termId, error: 'Could not fetch term details' }; + } + }) + ); + // If specific taxonomy requested if (params.taxonomy) { - const taxonomyField = params.taxonomy === 'category' ? 'categories' : - params.taxonomy === 'post_tag' ? 'tags' : - params.taxonomy; - - if (content[taxonomyField]) { - // Get full term details - const endpoint = getTaxonomyEndpoint(params.taxonomy); - const termDetails = await Promise.all( - content[taxonomyField].map(async (termId: number) => { - try { - return await makeWordPressRequest('GET', `${endpoint}/${termId}`); - } catch { - return { id: termId, error: 'Could not fetch term details' }; - } - }) - ); - terms[params.taxonomy] = termDetails; + const { slug, restBase } = await resolveTaxonomy(params.taxonomy, params.site_id); + const termIds = content[restBase]; + + if (Array.isArray(termIds) && termIds.length > 0) { + terms[slug] = await fetchTermDetails(restBase, termIds); + } else { + terms[slug] = []; } } else { // Get all taxonomy terms for this content + const taxonomies = await getTaxonomies(false, params.site_id); for (const [taxonomySlug, taxonomyInfo] of Object.entries(taxonomies)) { const tax = taxonomyInfo as any; // Check if this taxonomy applies to this content type if (tax.types && tax.types.includes(params.content_type)) { - const taxonomyField = taxonomySlug === 'category' ? 'categories' : - taxonomySlug === 'post_tag' ? 'tags' : - taxonomySlug; - - if (content[taxonomyField] && Array.isArray(content[taxonomyField]) && content[taxonomyField].length > 0) { - const endpoint = getTaxonomyEndpoint(taxonomySlug); - const termDetails = await Promise.all( - content[taxonomyField].map(async (termId: number) => { - try { - return await makeWordPressRequest('GET', `${endpoint}/${termId}`); - } catch { - return { id: termId, error: 'Could not fetch term details' }; - } - }) - ); - terms[taxonomySlug] = termDetails; + const restBase = tax.rest_base || taxonomySlug; + const termIds = content[restBase]; + + if (Array.isArray(termIds) && termIds.length > 0) { + terms[taxonomySlug] = await fetchTermDetails(restBase, termIds); } } } } - + return { toolResult: { - content: [{ - type: 'text', + content: [{ + type: 'text', text: JSON.stringify({ content_id: params.content_id, content_type: params.content_type, terms: terms - }, null, 2) + }, null, 2) }], isError: false } @@ -517,13 +547,13 @@ export const unifiedTaxonomyHandlers = { } catch (error: any) { return { toolResult: { - content: [{ - type: 'text', - text: `Error getting content terms: ${error.message}` + content: [{ + type: 'text', + text: `Error getting content terms: ${error.message}` }], isError: true } }; } } -}; \ No newline at end of file +};