From b28f3bea96e151a2cae18d418769b49ebbcb046b Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 7 May 2026 14:22:43 -0500 Subject: [PATCH] =?UTF-8?q?feat(tools):=20expand=20to=2067=20tools=20?= =?UTF-8?q?=E2=80=94=20full=20CRUD=20for=20contacts,=20banners,=20newsfeed?= =?UTF-8?q?s,=20tags,=20fields,=20menu=20items,=20messages,=20media,=20red?= =?UTF-8?q?irects,=20associations,=20checkin,=20and=20content=20history?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- src/index.ts | 541 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 541 insertions(+) diff --git a/src/index.ts b/src/index.ts index 81335b0..0f274cb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -633,6 +633,547 @@ server.tool( }, ); +// ── Contacts (CRUD) ───────────────────────────────────────────────────── + +server.tool( + 'joomla_contact_get', + 'Get a single contact by ID', + { + id: z.number().describe('Contact ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get(`/contact/${id}`)); + }, +); + +server.tool( + 'joomla_contact_create', + 'Create a new contact', + { + name: z.string().describe('Contact name'), + alias: z.string().optional().describe('URL alias'), + catid: z.number().optional().describe('Category ID'), + email_to: z.string().optional().describe('Email address'), + telephone: z.string().optional().describe('Phone number'), + address: z.string().optional().describe('Street address'), + suburb: z.string().optional().describe('City/suburb'), + state: z.string().optional().describe('State/province'), + postcode: z.string().optional().describe('Postal code'), + country_id: z.number().optional().describe('Country ID'), + published: z.number().optional().describe('1=published, 0=unpublished'), + language: z.string().optional().describe('Language code (default "*")'), + ...ConnectionParam, + }, + async ({ name, alias, catid, email_to, telephone, address, suburb, state, postcode, country_id, published, language, connection }) => { + const client = clientFor(connection); + const body: Record = { name, language: language ?? '*' }; + if (alias) body.alias = alias; + if (catid !== undefined) body.catid = catid; + if (email_to) body.email_to = email_to; + if (telephone) body.telephone = telephone; + if (address) body.address = address; + if (suburb) body.suburb = suburb; + if (state) body.state = state; + if (postcode) body.postcode = postcode; + if (country_id !== undefined) body.country_id = country_id; + if (published !== undefined) body.published = published; + return formatResponse(await client.post('/contact', body)); + }, +); + +server.tool( + 'joomla_contact_update', + 'Update an existing contact', + { + id: z.number().describe('Contact ID'), + name: z.string().optional().describe('Contact name'), + email_to: z.string().optional().describe('Email address'), + telephone: z.string().optional().describe('Phone number'), + address: z.string().optional().describe('Street address'), + published: z.number().optional().describe('1=published, 0=unpublished'), + ...ConnectionParam, + }, + async ({ id, name, email_to, telephone, address, published, connection }) => { + const client = clientFor(connection); + const body: Record = {}; + if (name !== undefined) body.name = name; + if (email_to !== undefined) body.email_to = email_to; + if (telephone !== undefined) body.telephone = telephone; + if (address !== undefined) body.address = address; + if (published !== undefined) body.published = published; + return formatResponse(await client.patch(`/contact/${id}`, body)); + }, +); + +server.tool( + 'joomla_contact_delete', + 'Delete a contact', + { + id: z.number().describe('Contact ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.delete(`/contact/${id}`)); + }, +); + +// ── Banners (CRUD) ────────────────────────────────────────────────────── + +server.tool( + 'joomla_banner_get', + 'Get a single banner by ID', + { + id: z.number().describe('Banner ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get(`/banners/${id}`)); + }, +); + +server.tool( + 'joomla_banner_create', + 'Create a new banner', + { + name: z.string().describe('Banner name'), + catid: z.number().optional().describe('Category ID'), + clickurl: z.string().optional().describe('Click URL'), + custombannercode: z.string().optional().describe('Custom HTML/code for the banner'), + state: z.number().optional().describe('1=published, 0=unpublished'), + ...ConnectionParam, + }, + async ({ name, catid, clickurl, custombannercode, state, connection }) => { + const client = clientFor(connection); + const body: Record = { name }; + if (catid !== undefined) body.catid = catid; + if (clickurl) body.clickurl = clickurl; + if (custombannercode) body.custombannercode = custombannercode; + if (state !== undefined) body.state = state; + return formatResponse(await client.post('/banners', body)); + }, +); + +server.tool( + 'joomla_banner_delete', + 'Delete a banner', + { + id: z.number().describe('Banner ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.delete(`/banners/${id}`)); + }, +); + +// ── Banner Clients ────────────────────────────────────────────────────── + +server.tool( + 'joomla_banner_clients_list', + 'List banner clients', + { ...ConnectionParam }, + async ({ connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get('/banners/clients')); + }, +); + +// ── Newsfeeds (CRUD) ──────────────────────────────────────────────────── + +server.tool( + 'joomla_newsfeed_get', + 'Get a single newsfeed by ID', + { + id: z.number().describe('Newsfeed ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get(`/newsfeeds/${id}`)); + }, +); + +server.tool( + 'joomla_newsfeed_create', + 'Create a new newsfeed', + { + name: z.string().describe('Feed name'), + link: z.string().describe('Feed URL'), + catid: z.number().describe('Category ID'), + numarticles: z.number().optional().describe('Number of articles to display'), + published: z.number().optional().describe('1=published, 0=unpublished'), + language: z.string().optional().describe('Language code (default "*")'), + ...ConnectionParam, + }, + async ({ name, link, catid, numarticles, published, language, connection }) => { + const client = clientFor(connection); + const body: Record = { name, link, catid, language: language ?? '*' }; + if (numarticles !== undefined) body.numarticles = numarticles; + if (published !== undefined) body.published = published; + return formatResponse(await client.post('/newsfeeds', body)); + }, +); + +server.tool( + 'joomla_newsfeed_delete', + 'Delete a newsfeed', + { + id: z.number().describe('Newsfeed ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.delete(`/newsfeeds/${id}`)); + }, +); + +// ── Tags (CRUD) ───────────────────────────────────────────────────────── + +server.tool( + 'joomla_tag_get', + 'Get a single tag by ID', + { + id: z.number().describe('Tag ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get(`/tags/${id}`)); + }, +); + +server.tool( + 'joomla_tag_update', + 'Update a tag', + { + id: z.number().describe('Tag ID'), + title: z.string().optional().describe('New tag title'), + published: z.number().optional().describe('1=published, 0=unpublished'), + ...ConnectionParam, + }, + async ({ id, title, published, connection }) => { + const client = clientFor(connection); + const body: Record = {}; + if (title !== undefined) body.title = title; + if (published !== undefined) body.published = published; + return formatResponse(await client.patch(`/tags/${id}`, body)); + }, +); + +server.tool( + 'joomla_tag_delete', + 'Delete a tag', + { + id: z.number().describe('Tag ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.delete(`/tags/${id}`)); + }, +); + +// ── Custom Fields (CRUD) ──────────────────────────────────────────────── + +server.tool( + 'joomla_field_get', + 'Get a single custom field by ID', + { + id: z.number().describe('Field ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get(`/fields/com_content.article/${id}`)); + }, +); + +server.tool( + 'joomla_field_create', + 'Create a custom field', + { + title: z.string().describe('Field title'), + name: z.string().describe('Field name (system identifier)'), + type: z.string().describe('Field type (text, textarea, list, radio, checkboxes, etc.)'), + context: z.string().optional().describe('Context (default "com_content.article")'), + label: z.string().optional().describe('Display label'), + description: z.string().optional().describe('Field description'), + required: z.number().optional().describe('1=required, 0=optional'), + state: z.number().optional().describe('1=published, 0=unpublished'), + ...ConnectionParam, + }, + async ({ title, name, type, context, label, description, required: req, state, connection }) => { + const client = clientFor(connection); + const ctx = context ?? 'com_content.article'; + const body: Record = { title, name, type }; + if (label) body.label = label; + if (description) body.description = description; + if (req !== undefined) body.required = req; + if (state !== undefined) body.state = state; + return formatResponse(await client.post(`/fields/${ctx}`, body)); + }, +); + +server.tool( + 'joomla_field_delete', + 'Delete a custom field', + { + id: z.number().describe('Field ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.delete(`/fields/com_content.article/${id}`)); + }, +); + +// ── Menu Items (CRUD) ─────────────────────────────────────────────────── + +server.tool( + 'joomla_menu_item_create', + 'Create a new menu item', + { + title: z.string().describe('Menu item title'), + menutype: z.string().describe('Menu type alias (e.g. "mainmenu")'), + type: z.string().describe('Menu item type (e.g. "component", "url", "alias", "separator", "heading")'), + link: z.string().optional().describe('URL or component link'), + parent_id: z.number().optional().describe('Parent menu item ID (default 1 = root)'), + published: z.number().optional().describe('1=published, 0=unpublished'), + access: z.number().optional().describe('Access level ID'), + language: z.string().optional().describe('Language code (default "*")'), + ...ConnectionParam, + }, + async ({ title, menutype, type, link, parent_id, published, access, language, connection }) => { + const client = clientFor(connection); + const body: Record = { + title, + menutype, + type, + language: language ?? '*', + }; + if (link) body.link = link; + if (parent_id !== undefined) body.parent_id = parent_id; + if (published !== undefined) body.published = published; + if (access !== undefined) body.access = access; + return formatResponse(await client.post('/menus/items', body)); + }, +); + +server.tool( + 'joomla_menu_item_update', + 'Update a menu item', + { + id: z.number().describe('Menu item ID'), + title: z.string().optional().describe('New title'), + link: z.string().optional().describe('New link URL'), + published: z.number().optional().describe('1=published, 0=unpublished'), + parent_id: z.number().optional().describe('New parent ID'), + ...ConnectionParam, + }, + async ({ id, title, link, published, parent_id, connection }) => { + const client = clientFor(connection); + const body: Record = {}; + if (title !== undefined) body.title = title; + if (link !== undefined) body.link = link; + if (published !== undefined) body.published = published; + if (parent_id !== undefined) body.parent_id = parent_id; + return formatResponse(await client.patch(`/menus/items/${id}`, body)); + }, +); + +server.tool( + 'joomla_menu_item_delete', + 'Delete a menu item', + { + id: z.number().describe('Menu item ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.delete(`/menus/items/${id}`)); + }, +); + +// ── Messages (Send) ──────────────────────────────────────────────────── + +server.tool( + 'joomla_message_send', + 'Send a private message to a Joomla user', + { + user_id_to: z.number().describe('Recipient user ID'), + subject: z.string().describe('Message subject'), + message: z.string().describe('Message body'), + ...ConnectionParam, + }, + async ({ user_id_to, subject, message, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.post('/messages', { user_id_to, subject, message })); + }, +); + +server.tool( + 'joomla_message_delete', + 'Delete a private message', + { + id: z.number().describe('Message ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.delete(`/messages/${id}`)); + }, +); + +// ── Media (Upload/Delete) ─────────────────────────────────────────────── + +server.tool( + 'joomla_media_file_get', + 'Get metadata for a specific media file', + { + path: z.string().describe('File path relative to media root (e.g. "images/logo.png")'), + ...ConnectionParam, + }, + async ({ path, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get(`/media/files/${encodeURIComponent(path)}`)); + }, +); + +server.tool( + 'joomla_media_file_delete', + 'Delete a media file', + { + path: z.string().describe('File path relative to media root'), + ...ConnectionParam, + }, + async ({ path, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.delete(`/media/files/${encodeURIComponent(path)}`)); + }, +); + +server.tool( + 'joomla_media_folder_create', + 'Create a new media folder', + { + path: z.string().describe('Full folder path to create (e.g. "images/photos/2026")'), + ...ConnectionParam, + }, + async ({ path, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.post('/media/files', { path })); + }, +); + +// ── Content History ───────────────────────────────────────────────────── + +server.tool( + 'joomla_content_history_list', + 'List version history for a content item', + { + type_alias: z.string().describe('Content type alias (e.g. "com_content.article")'), + item_id: z.number().describe('Item ID'), + ...ConnectionParam, + }, + async ({ type_alias, item_id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get('/content/history', { + 'filter[type_alias]': type_alias, + 'filter[item_id]': String(item_id), + })); + }, +); + +// ── Checkin ───────────────────────────────────────────────────────────── + +server.tool( + 'joomla_checkin', + 'Check in (unlock) a content item that is checked out', + { + context: z.string().describe('Context (e.g. "com_content.article")'), + id: z.number().describe('Item ID to check in'), + ...ConnectionParam, + }, + async ({ context, id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.post(`/checkin/${context}/${id}`, {})); + }, +); + +// ── Redirects ─────────────────────────────────────────────────────────── + +server.tool( + 'joomla_redirects_list', + 'List URL redirects', + { + search: z.string().optional().describe('Search in old URL'), + state: z.enum(['0', '1', '2', '-2']).optional().describe('0=disabled, 1=enabled, 2=archived, -2=trashed'), + ...ConnectionParam, + }, + async ({ search, state, connection }) => { + const client = clientFor(connection); + const params: Record = {}; + if (search) params['filter[search]'] = search; + if (state !== undefined) params['filter[state]'] = state; + return formatResponse(await client.get('/redirects', params)); + }, +); + +server.tool( + 'joomla_redirect_create', + 'Create a URL redirect', + { + old_url: z.string().describe('Source URL to redirect from'), + new_url: z.string().describe('Destination URL to redirect to'), + status_code: z.enum(['301', '302']).optional().describe('301=permanent, 302=temporary (default 301)'), + published: z.number().optional().describe('1=enabled, 0=disabled'), + ...ConnectionParam, + }, + async ({ old_url, new_url, status_code, published, connection }) => { + const client = clientFor(connection); + const body: Record = { + old_url, + new_url, + header: status_code ? Number(status_code) : 301, + published: published ?? 1, + }; + return formatResponse(await client.post('/redirects', body)); + }, +); + +server.tool( + 'joomla_redirect_delete', + 'Delete a URL redirect', + { + id: z.number().describe('Redirect ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.delete(`/redirects/${id}`)); + }, +); + +// ── Associations (Multilingual) ───────────────────────────────────────── + +server.tool( + 'joomla_associations_list', + 'List multilingual associations for a content type', + { + context: z.string().describe('Context (e.g. "com_content.article")'), + id: z.number().describe('Item ID to get associations for'), + ...ConnectionParam, + }, + async ({ context, id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get(`/associations/${context}/${id}`)); + }, +); + // ── Generic API Call ──────────────────────────────────────────────────── server.tool(