You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
> **v2.2.0 breaking change.** Tools were renamed from flat names (`publish`, `schedule`, ...) to dot-notation (`post.publish`, `post.schedule`, ...). Update any prompts, agent skills, or n8n nodes that referenced the old names.
105
+
> **v2.2.0 breaking change.** Tools were renamed from flat names (`publish`, `schedule`, ...) to dot-notation (`post_publish`, `post_schedule`, ...). Update any prompts, agent skills, or n8n nodes that referenced the old names.
106
106
107
107
## Channels
108
108
@@ -120,7 +120,7 @@ Eight tools, dot-notation names form a navigable tree (`post.*`, `channel.*`, `p
120
120
## Example agent call
121
121
122
122
```jsonc
123
-
// post.publish tool
123
+
// post_publish tool
124
124
{
125
125
"content": {
126
126
"id": "n8n-webhook-setup@2026-05-20",
@@ -153,18 +153,18 @@ Eight tools, dot-notation names form a navigable tree (`post.*`, `channel.*`, `p
153
153
154
154
## Idempotency
155
155
156
-
Re-running `post.publish` with the same `content.id` + `channel` pair returns the existing `live_url` immediately without making another platform API call. Safe to retry on failure.
156
+
Re-running `post_publish` with the same `content.id` + `channel` pair returns the existing `live_url` immediately without making another platform API call. Safe to retry on failure.
157
157
158
158
## Scheduling
159
159
160
-
Variants with `schedule_at` (ISO-8601 with timezone, e.g. `"2026-05-21T09:00:00+00:00"`) are stored in `~/.distribution-mcp/scheduled.yaml` and fired on the next `post.drain` call. Run `drain` from cron:
160
+
Variants with `schedule_at` (ISO-8601 with timezone, e.g. `"2026-05-21T09:00:00+00:00"`) are stored in `~/.distribution-mcp/scheduled.yaml` and fired on the next `post_drain` call. Run `drain` from cron:
161
161
162
162
```bash
163
163
# fire due posts every 5 minutes
164
164
*/5 * * * * npx -y content-distribution-mcp drain
165
165
```
166
166
167
-
Or call the `post.drain` MCP tool directly from an agent.
167
+
Or call the `post_drain` MCP tool directly from an agent.
channel: z.string().describe("Channel slug in the form 'platform' or 'platform:account', e.g. 'devto:main', 'reddit:ClaudeAI', 'linkedin:personal'. Use subreddit.list for reddit subreddit options."),
33
+
channel: z.string().describe("Channel slug in the form 'platform' or 'platform:account', e.g. 'devto:main', 'reddit:ClaudeAI', 'linkedin:personal'. Use subreddit_list for reddit subreddit options."),
34
34
title: z.string().describe("Channel-specific title for this variant; can differ from content.title to fit platform norms, e.g. shorter for DEV.to or question-form for Reddit."),
35
-
body: z.string().describe("Channel-adapted body in Markdown or plain text per channel. Use channel.hints to check whether the channel supports Markdown."),
35
+
body: z.string().describe("Channel-adapted body in Markdown or plain text per channel. Use channel_hints to check whether the channel supports Markdown."),
36
36
tags: z.array(z.string()).default([]).describe("Channel-specific tags for this variant; overrides content.tags when present; each adapter truncates or converts to platform limits."),
37
37
canonical_url: z.string().url().optional().describe("Canonical URL override for this variant; overrides content.canonical_url for this channel only when set."),
38
38
cta_block: z.string().optional().describe("CTA block override for this variant; overrides content.cta_block for this channel only; appended to body before publishing."),
39
39
schedule_at: z.string().optional().describe("ISO-8601 datetime with timezone offset for future publishing, e.g. '2026-05-21T09:00:00+01:00'. Omit for immediate publishing."),
40
-
extras: z.record(z.unknown()).default({}).describe("Channel-specific knobs: flair (Reddit), category (GitHub Discussions), repo and series (Hashnode). Keys and types vary by adapter; use channel.hints to discover supported extras."),
40
+
extras: z.record(z.unknown()).default({}).describe("Channel-specific knobs: flair (Reddit), category (GitHub Discussions), repo and series (Hashnode). Keys and types vary by adapter; use channel_hints to discover supported extras."),
41
41
});
42
42
43
43
// --- Output schemas (raw shapes for registerTool) ----------------------------
@@ -65,7 +65,7 @@ const statusEntryShape = {
65
65
live_url: z.string().nullable().describe("Public URL of the live post; null when not yet published."),
66
66
published_at: z.string().nullable().describe("UTC ISO-8601 timestamp of the successful publish; null when not yet live."),
67
67
error: z.string().nullable().describe("Last error message from the platform; null when no error."),
68
-
content_id: z.string().describe("Stable content identifier passed to post.publish / post.schedule; matches the content.id field."),
68
+
content_id: z.string().describe("Stable content identifier passed to post_publish / post_schedule; matches the content.id field."),
69
69
retry_count: z.number().nullable().describe("Number of publish attempts made so far; null for first attempt."),
70
70
next_retry_at: z.string().nullable().describe("UTC ISO-8601 of the next scheduled retry; null when not queued for retry."),
71
71
}asconst;
@@ -134,16 +134,16 @@ export function createServer() {
134
134
constadapters=buildAdapterMap();
135
135
constbackend=buildBackend();
136
136
137
-
// --- post.publish ---
137
+
// --- post_publish ---
138
138
server.registerTool(
139
-
"post.publish",
139
+
"post_publish",
140
140
{
141
141
title: "Publish variants to one or more channels immediately",
142
-
description: "Publish one or more channel variants immediately. Side effects: makes external HTTP requests to each channel platform; writes publish state to the local YAML backend; requires valid credentials in the named profile. Idempotent on (content.id, channel) — re-running with the same IDs returns cached state without re-posting. Use post.publish for immediate-only delivery; use post.schedule when any variant needs a future schedule_at; use post.drain to flush a previously built queue.",
142
+
description: "Publish one or more channel variants immediately. Side effects: makes external HTTP requests to each channel platform; writes publish state to the local YAML backend; requires valid credentials in the named profile. Idempotent on (content.id, channel) — re-running with the same IDs returns cached state without re-posting. Use post_publish for immediate-only delivery; use post_schedule when any variant needs a future schedule_at; use post_drain to flush a previously built queue.",
143
143
inputSchema: {
144
144
content: ContentSchema.describe("Content piece to publish: stable id (idempotency key), title, body_md, tags, and optional cover_image / canonical_url / cta_block / author fields. The id + channel pair is the deduplication key — the same id will not be re-posted."),
145
-
variants: z.array(VariantSchema).describe("One or more channel-specific publish targets. Each entry specifies the channel slug (e.g. 'devto:main', 'reddit:ClaudeAI'), the adapted title and body, optional schedule_at for future delivery, and channel extras such as flair. Use channel.hints to check per-channel constraints before composing."),
146
-
profile_name: z.string().describe("Name of the distribution profile (credentials store). Use profile.list to discover available names."),
145
+
variants: z.array(VariantSchema).describe("One or more channel-specific publish targets. Each entry specifies the channel slug (e.g. 'devto:main', 'reddit:ClaudeAI'), the adapted title and body, optional schedule_at for future delivery, and channel extras such as flair. Use channel_hints to check per-channel constraints before composing."),
146
+
profile_name: z.string().describe("Name of the distribution profile (credentials store). Use profile_list to discover available names."),
147
147
},
148
148
outputSchema: publishOutputShape,
149
149
annotations: {
@@ -161,16 +161,16 @@ export function createServer() {
161
161
},
162
162
);
163
163
164
-
// --- post.schedule ---
164
+
// --- post_schedule ---
165
165
server.registerTool(
166
-
"post.schedule",
166
+
"post_schedule",
167
167
{
168
168
title: "Schedule variants for future publishing",
169
-
description: "Enqueue channel variants with schedule_at for future publishing; variants without schedule_at are published immediately. Side effects: writes entries to the local YAML schedule store; makes external HTTP requests for any immediately-published variants; requires credentials in the named profile. Idempotent on (content.id, channel). Use post.schedule when any variant needs a future publish time; use post.publish for all-immediate delivery; use post.drain to process the scheduled queue later.",
169
+
description: "Enqueue channel variants with schedule_at for future publishing; variants without schedule_at are published immediately. Side effects: writes entries to the local YAML schedule store; makes external HTTP requests for any immediately-published variants; requires credentials in the named profile. Idempotent on (content.id, channel). Use post_schedule when any variant needs a future publish time; use post_publish for all-immediate delivery; use post_drain to process the scheduled queue later.",
170
170
inputSchema: {
171
171
content: ContentSchema.describe("Content piece to publish: stable id (idempotency key), title, body_md, tags, and optional cover_image / canonical_url / cta_block / author fields. The id + channel pair is the deduplication key — the same id will not be re-posted."),
172
-
variants: z.array(VariantSchema).describe("One or more channel-specific publish targets. Each entry specifies the channel slug, the adapted title and body, and optionally schedule_at (ISO-8601 with timezone) for future delivery. Variants without schedule_at are published immediately; variants with schedule_at are queued for post.drain. Use channel.hints to check per-channel constraints."),
173
-
profile_name: z.string().describe("Name of the distribution profile (credentials store). Use profile.list to discover available names."),
172
+
variants: z.array(VariantSchema).describe("One or more channel-specific publish targets. Each entry specifies the channel slug, the adapted title and body, and optionally schedule_at (ISO-8601 with timezone) for future delivery. Variants without schedule_at are published immediately; variants with schedule_at are queued for post_drain. Use channel_hints to check per-channel constraints."),
173
+
profile_name: z.string().describe("Name of the distribution profile (credentials store). Use profile_list to discover available names."),
174
174
},
175
175
outputSchema: scheduleOutputShape,
176
176
annotations: {
@@ -188,12 +188,12 @@ export function createServer() {
188
188
},
189
189
);
190
190
191
-
// --- post.drain ---
191
+
// --- post_drain ---
192
192
server.registerTool(
193
-
"post.drain",
193
+
"post_drain",
194
194
{
195
195
title: "Fire all scheduled posts due now",
196
-
description: "Fire all scheduled posts due at or before the given time boundary. Side effects: makes external HTTP requests for each due entry; writes results to the YAML backend. Idempotent — already-published (content.id, channel) pairs are skipped; no-op when no entries are due. Safe to call from cron. Use post.drain on a recurring schedule to flush the queue; use post.publish or post.schedule to add new content; use post.status to inspect results after drain runs.",
196
+
description: "Fire all scheduled posts due at or before the given time boundary. Side effects: makes external HTTP requests for each due entry; writes results to the YAML backend. Idempotent — already-published (content.id, channel) pairs are skipped; no-op when no entries are due. Safe to call from cron. Use post_drain on a recurring schedule to flush the queue; use post_publish or post_schedule to add new content; use post_status to inspect results after drain runs.",
197
197
inputSchema: {
198
198
now: z.string().optional().describe("ISO-8601 datetime boundary, e.g. '2026-05-21T09:00:00Z'; defaults to current UTC time when omitted."),
199
199
},
@@ -212,12 +212,12 @@ export function createServer() {
212
212
},
213
213
);
214
214
215
-
// --- post.status ---
215
+
// --- post_status ---
216
216
server.registerTool(
217
-
"post.status",
217
+
"post_status",
218
218
{
219
219
title: "Read publish state for content pieces",
220
-
description: "Return publish state for content pieces. Filters by content_id, channel, or both; returns all entries when neither is given. Side effects: read-only; no external HTTP calls; no auth needed. Deterministic given unchanged backend state. Use post.status to inspect what has been published, what is queued, or what errored; use post.publish, post.schedule, or post.drain to change state.",
220
+
description: "Return publish state for content pieces. Filters by content_id, channel, or both; returns all entries when neither is given. Side effects: read-only; no external HTTP calls; no auth needed. Deterministic given unchanged backend state. Use post_status to inspect what has been published, what is queued, or what errored; use post_publish, post_schedule, or post_drain to change state.",
221
221
inputSchema: {
222
222
content_id: z.string().optional().describe("Filter to a specific content piece by its stable ID; omit to return state for all content."),
223
223
channel: z.string().optional().describe("Filter to a specific channel slug, e.g. 'devto', 'reddit:ClaudeAI'; omit to return state for all channels."),
@@ -247,12 +247,12 @@ export function createServer() {
247
247
},
248
248
);
249
249
250
-
// --- post.unpublish ---
250
+
// --- post_unpublish ---
251
251
server.registerTool(
252
-
"post.unpublish",
252
+
"post_unpublish",
253
253
{
254
254
title: "Retract a published post (best-effort)",
255
-
description: "Best-effort delete of a published post on the target platform. Side effects: makes an external HTTP DELETE or update request; DEV.to sets published=false (soft delete); platforms without a delete API return success=false without error. Non-idempotent — calling on an already-deleted URL may return a platform 404. Use post.unpublish to retract a live post; use post.status first to obtain the live_url; use post.publish to re-publish after an unpublish.",
255
+
description: "Best-effort delete of a published post on the target platform. Side effects: makes an external HTTP DELETE or update request; DEV.to sets published=false (soft delete); platforms without a delete API return success=false without error. Non-idempotent — calling on an already-deleted URL may return a platform 404. Use post_unpublish to retract a live post; use post_status first to obtain the live_url; use post_publish to re-publish after an unpublish.",
256
256
inputSchema: {
257
257
live_url: z.string().describe("URL of the live published post to retract, e.g. 'https://dev.to/user/post-slug'."),
258
258
channel: z.string().describe("Channel slug the post was published to, e.g. 'devto', 'hashnode', 'reddit:ClaudeAI'."),
@@ -284,12 +284,12 @@ export function createServer() {
284
284
},
285
285
);
286
286
287
-
// --- channel.hints ---
287
+
// --- channel_hints ---
288
288
server.registerTool(
289
-
"channel.hints",
289
+
"channel_hints",
290
290
{
291
291
title: "Static per-channel metadata",
292
-
description: "Return static per-channel metadata: character limits, Markdown support flags, tag vocabulary, and CTA placement rules. Side effects: read-only; no external HTTP calls; no auth needed. Fully deterministic — returns compile-time adapter constants. Use channel.hints before composing a variant body to understand channel constraints; use post.publish or post.schedule once you have a valid variant.",
292
+
description: "Return static per-channel metadata: character limits, Markdown support flags, tag vocabulary, and CTA placement rules. Side effects: read-only; no external HTTP calls; no auth needed. Fully deterministic — returns compile-time adapter constants. Use channel_hints before composing a variant body to understand channel constraints; use post_publish or post_schedule once you have a valid variant.",
293
293
inputSchema: {
294
294
channel: z.string().describe("Channel platform name, e.g. 'devto', 'reddit', 'hashnode', 'bluesky'. Use the platform prefix only, not the full 'platform:account' form."),
295
295
},
@@ -312,12 +312,12 @@ export function createServer() {
312
312
},
313
313
);
314
314
315
-
// --- profile.list ---
315
+
// --- profile_list ---
316
316
server.registerTool(
317
-
"profile.list",
317
+
"profile_list",
318
318
{
319
319
title: "List configured distribution profiles",
320
-
description: "Return all distribution profile names configured in the YAML backend. Side effects: read-only; no external HTTP calls. Deterministic given backend state. Use profile.list to discover available profiles before calling post.publish, post.schedule, or subreddit.list; then pass the chosen name as profile_name.",
320
+
description: "Return all distribution profile names configured in the YAML backend. Side effects: read-only; no external HTTP calls. Deterministic given backend state. Use profile_list to discover available profiles before calling post_publish, post_schedule, or subreddit_list; then pass the chosen name as profile_name.",
321
321
inputSchema: {},
322
322
outputSchema: profileListOutputShape,
323
323
annotations: {
@@ -334,12 +334,12 @@ export function createServer() {
334
334
},
335
335
);
336
336
337
-
// --- subreddit.list ---
337
+
// --- subreddit_list ---
338
338
server.registerTool(
339
-
"subreddit.list",
339
+
"subreddit_list",
340
340
{
341
341
title: "List Subreddit Catalog entries",
342
-
description: "Return all subreddits in the Subreddit Catalog with cooldown windows, flair vocabulary, and last-posted metadata. Optionally filtered to subreddits allowed by the named profile. Side effects: read-only; no external HTTP calls. Deterministic given backend state. Use subreddit.list to select a subreddit and obtain flair IDs before composing a reddit: channel variant; pass flair in variant.extras.flair.",
342
+
description: "Return all subreddits in the Subreddit Catalog with cooldown windows, flair vocabulary, and last-posted metadata. Optionally filtered to subreddits allowed by the named profile. Side effects: read-only; no external HTTP calls. Deterministic given backend state. Use subreddit_list to select a subreddit and obtain flair IDs before composing a reddit: channel variant; pass flair in variant.extras.flair.",
343
343
inputSchema: {
344
344
profile_name: z.string().optional().describe("Optional profile name to filter subreddits to those allowed by that profile; omit to return the full catalog."),
0 commit comments