Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 41 additions & 6 deletions src/tools/posts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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<string, string> = {};
// 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: [
{
Expand All @@ -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<string, string> = {};
// 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: [
{
Expand Down