create_draft
Create a draft Substack post from markdown content without publishing. Converts markdown to Substack's format and saves as unpublished draft.
Instructions
Create a new draft post. Accepts markdown body which is converted to Substack's format. Does NOT publish — creates a draft only.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| title | Yes | Post title | |
| body | No | Post body in markdown format | |
| subtitle | No | Post subtitle | |
| audience | No | Who can see this post | everyone |
Implementation Reference
- src/server.ts:180-204 (handler)Handler function for 'create_draft' tool. Converts markdown body to ProseMirror format, calls client.createDraft() with title, body, subtitle, and audience parameters, and returns the draft ID and title.
async ({ title, body, subtitle, audience }) => { const prosemirrorBody = body ? markdownToProseMirror(body) : undefined; const draft = await client.createDraft( title, prosemirrorBody, subtitle, audience, ); return { content: [ { type: "text", text: JSON.stringify( { id: draft.id, title: draft.draft_title, message: "Draft created successfully. Open Substack to review and publish.", }, null, 2, ), }, ], }; }, - src/server.ts:170-179 (schema)Zod schema defining input parameters for 'create_draft' tool: title (required string), body (optional markdown), subtitle (optional string), and audience (optional enum with default 'everyone').
{ title: z.string().describe("Post title"), body: z.string().optional().describe("Post body in markdown format"), subtitle: z.string().optional().describe("Post subtitle"), audience: z .enum(["everyone", "only_paid", "founding", "only_free"]) .optional() .default("everyone") .describe("Who can see this post"), }, - src/server.ts:167-205 (registration)Registration of 'create_draft' tool with MCP server. Includes tool name, description, schema, and handler function.
server.tool( "create_draft", "Create a new draft post. Accepts markdown body which is converted to Substack's format. Does NOT publish — creates a draft only.", { title: z.string().describe("Post title"), body: z.string().optional().describe("Post body in markdown format"), subtitle: z.string().optional().describe("Post subtitle"), audience: z .enum(["everyone", "only_paid", "founding", "only_free"]) .optional() .default("everyone") .describe("Who can see this post"), }, async ({ title, body, subtitle, audience }) => { const prosemirrorBody = body ? markdownToProseMirror(body) : undefined; const draft = await client.createDraft( title, prosemirrorBody, subtitle, audience, ); return { content: [ { type: "text", text: JSON.stringify( { id: draft.id, title: draft.draft_title, message: "Draft created successfully. Open Substack to review and publish.", }, null, 2, ), }, ], }; }, ); - src/api/client.ts:123-146 (helper)createDraft method in SubstackClient that constructs the API payload and makes POST request to /api/v1/drafts endpoint to create a new draft.
async createDraft( title: string, body?: string, subtitle?: string, audience: string = "everyone", ): Promise<SubstackDraft> { const payload: DraftCreatePayload = { draft_title: title, draft_bylines: [{ id: this.userId, is_guest: false }], audience: audience as DraftCreatePayload["audience"], type: "newsletter", }; if (subtitle) payload.draft_subtitle = subtitle; if (body) payload.draft_body = body; return this.request<SubstackDraft>( `${this.publicationUrl}/api/v1/drafts`, { method: "POST", body: JSON.stringify(payload), }, ); } - src/api/types.ts:53-61 (schema)DraftCreatePayload interface defining the structure for creating drafts: draft_title, draft_subtitle, draft_body, draft_bylines, audience, type, and section_id.
export interface DraftCreatePayload { draft_title: string; draft_subtitle?: string; draft_body?: string; draft_bylines: Array<{ id: number; is_guest: boolean }>; audience?: "everyone" | "only_paid" | "founding" | "only_free"; type?: "newsletter" | "podcast" | "thread"; section_id?: number | null; }