From 3554c96614660958d814fa3304ff3d6dfe879a7a Mon Sep 17 00:00:00 2001 From: Kenneth Bennett Date: Tue, 9 Jun 2026 16:07:39 -0400 Subject: [PATCH] feat(posts): add newsletter + email_segment params to posts_add and posts_edit Forward Ghost's newsletter and email_segment query parameters through the @tryghost/admin-api SDK's query-params argument so that publishing via this MCP can trigger the subscriber email send. Kept in a dedicated emailSendParams schema fragment (not postMutableFields) because they are send-control query parameters, not post body fields. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/tools/posts.ts | 47 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/src/tools/posts.ts b/src/tools/posts.ts index 07f5220..981d4c3 100644 --- a/src/tools/posts.ts +++ b/src/tools/posts.ts @@ -60,15 +60,30 @@ const postMutableFields = { tags: z.array(tagRef).optional(), authors: z.array(authorRef).optional(), }; +// Email send-control fields. Unlike postMutableFields, these are Ghost Admin API +// QUERY parameters, not post body fields. They control whether a post is sent as +// email to newsletter subscribers when it transitions to published, and to which +// subscriber segment. The handlers forward them via the SDK's queryParams argument, +// never in the post body. See https://docs.ghost.org/admin-api/posts/sending-a-post +const emailSendParams = { + newsletter: z.string().optional().describe( + "Slug of the newsletter to send this post as email when it transitions to published (e.g. 'default-newsletter'). When omitted, no email is sent." + ), + email_segment: z.string().optional().describe( + "NQL filter limiting which subscribers receive the email (e.g. 'all', 'status:free', 'status:-free', 'label:vip'). Requires newsletter to be set. Defaults to 'all' on the Ghost side." + ), +}; const addParams = { title: z.string(), ...postMutableFields, + ...emailSendParams, }; const editParams = { id: z.string(), updated_at: z.string(), title: z.string().optional(), ...postMutableFields, + ...emailSendParams, }; const deleteParams = { id: z.string(), @@ -114,9 +129,18 @@ export function registerPostTools(server: McpServer) { "posts_add", addParams, async (args, _extra) => { - // If html is present, use source: "html" to ensure Ghost uses the html content - const options = args.html ? { source: "html" } : undefined; - const post = await ghostApiClient.posts.add(args, options); + // newsletter + email_segment are Ghost query params, not post body fields: + // keep them out of the body and forward them via the SDK's queryParams arg. + const { newsletter, email_segment, ...postData } = args; + const options: Record = {}; + // If html is present, use source: "html" so Ghost uses the html content + if (postData.html) options.source = "html"; + if (newsletter) options.newsletter = newsletter; + if (email_segment) options.email_segment = email_segment; + const post = await ghostApiClient.posts.add( + postData, + Object.keys(options).length > 0 ? options : undefined + ); return { content: [ { @@ -133,9 +157,20 @@ export function registerPostTools(server: McpServer) { "posts_edit", editParams, async (args, _extra) => { - // If html is present, use source: "html" to ensure Ghost uses the html content for updates - const options = args.html ? { source: "html" } : undefined; - const post = await ghostApiClient.posts.edit(args, options); + // newsletter + email_segment are Ghost query params, not post body fields: + // keep them out of the body and forward them via the SDK's queryParams arg. + // This is what fires the subscriber post-as-email on the draft to published + // transition. See https://docs.ghost.org/admin-api/posts/sending-a-post + const { newsletter, email_segment, ...postData } = args; + const options: Record = {}; + // If html is present, use source: "html" so Ghost uses the html content for updates + if (postData.html) options.source = "html"; + if (newsletter) options.newsletter = newsletter; + if (email_segment) options.email_segment = email_segment; + const post = await ghostApiClient.posts.edit( + postData, + Object.keys(options).length > 0 ? options : undefined + ); return { content: [ {