create_note_with_link
Publish a Substack Note immediately with a linked URL displayed as a rich card below the text.
Instructions
Create a Substack Note with a link attachment. The link is displayed as a rich card below the note text. Publishes immediately.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| body | Yes | Note content in markdown format | |
| url | Yes | URL to attach as a link card |
Implementation Reference
- src/server.ts:300-334 (handler)Tool registration and handler for 'create_note_with_link'. Calls createNoteAttachment to upload the URL, converts markdown body to ProseMirror, then calls createNote with the attachment ID.
server.tool( "create_note_with_link", "Create a Substack Note with a link attachment. The link is displayed as a rich card below the note text. Publishes immediately.", { body: z.string().describe("Note content in markdown format"), url: z.string().url().describe("URL to attach as a link card"), }, async ({ body, url }) => { const attachment = await client.createNoteAttachment(url); const bodyJson = { type: "doc" as const, attrs: { schemaVersion: "v1" as const }, content: markdownToProseMirrorContent(body), }; const note = await client.createNote(bodyJson, [attachment.id]); return { content: [ { type: "text", text: JSON.stringify( { id: note.id, body: note.body, date: note.date, attachment_id: attachment.id, message: "Note with link published successfully.", }, null, 2, ), }, ], }; }, ); - src/server.ts:303-306 (schema)Input schema for the tool: 'body' (string, markdown) and 'url' (string, URL format) validated via Zod.
{ body: z.string().describe("Note content in markdown format"), url: z.string().url().describe("URL to attach as a link card"), }, - src/server.ts:300-334 (registration)Registration as an MCP tool via server.tool() in the createServer function.
server.tool( "create_note_with_link", "Create a Substack Note with a link attachment. The link is displayed as a rich card below the note text. Publishes immediately.", { body: z.string().describe("Note content in markdown format"), url: z.string().url().describe("URL to attach as a link card"), }, async ({ body, url }) => { const attachment = await client.createNoteAttachment(url); const bodyJson = { type: "doc" as const, attrs: { schemaVersion: "v1" as const }, content: markdownToProseMirrorContent(body), }; const note = await client.createNote(bodyJson, [attachment.id]); return { content: [ { type: "text", text: JSON.stringify( { id: note.id, body: note.body, date: note.date, attachment_id: attachment.id, message: "Note with link published successfully.", }, null, 2, ), }, ], }; }, ); - src/api/client.ts:204-212 (helper)Helper method createNoteAttachment() that POSTs the URL to /api/v1/comment/attachment to create a link attachment and returns a NoteAttachment with id.
async createNoteAttachment(url: string): Promise<NoteAttachment> { return this.request<NoteAttachment>( `${this.publicationUrl}/api/v1/comment/attachment`, { method: "POST", body: JSON.stringify({ url, type: "link" }), }, ); } - src/api/client.ts:182-202 (helper)Helper method createNote() that POSTs the note body JSON (with optional attachmentIds) to /api/v1/comment/feed.
async createNote( bodyJson: NoteCreatePayload["bodyJson"], attachmentIds?: string[], ): Promise<SubstackNote> { const payload: NoteCreatePayload = { bodyJson, tabId: "for-you", surface: "feed", replyMinimumRole: "everyone", }; if (attachmentIds?.length) { payload.attachmentIds = attachmentIds; } return this.request<SubstackNote>( `${this.publicationUrl}/api/v1/comment/feed`, { method: "POST", body: JSON.stringify(payload), }, ); }