Skip to content

Commit 8d7daab

Browse files
fix: rename dotted MCP tool names to underscores for Anthropic API compat
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 260a7dd commit 8d7daab

2 files changed

Lines changed: 45 additions & 45 deletions

File tree

README.md

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -93,16 +93,16 @@ Eight tools, dot-notation names form a navigable tree (`post.*`, `channel.*`, `p
9393

9494
| Tool | Purpose |
9595
|---|---|
96-
| `post.publish` | Immediate publish; idempotent on `(content.id, channel)` |
97-
| `post.schedule` | Queue variants for `schedule_at`, publish the rest immediately |
98-
| `post.drain` | Fire all scheduled posts due now — run from cron |
99-
| `post.status` | Per-channel state for a content piece or channel |
100-
| `post.unpublish` | Best-effort delete (DEV.to sets unpublished; others vary) |
101-
| `channel.hints` | Per-channel metadata: char limits, Markdown support, tag vocab |
102-
| `profile.list` | Names of configured distribution profiles |
103-
| `subreddit.list` | Subreddit Catalog: cooldowns, flair vocab, last-posted |
96+
| `post_publish` | Immediate publish; idempotent on `(content.id, channel)` |
97+
| `post_schedule` | Queue variants for `schedule_at`, publish the rest immediately |
98+
| `post_drain` | Fire all scheduled posts due now — run from cron |
99+
| `post_status` | Per-channel state for a content piece or channel |
100+
| `post_unpublish` | Best-effort delete (DEV.to sets unpublished; others vary) |
101+
| `channel_hints` | Per-channel metadata: char limits, Markdown support, tag vocab |
102+
| `profile_list` | Names of configured distribution profiles |
103+
| `subreddit_list` | Subreddit Catalog: cooldowns, flair vocab, last-posted |
104104

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.
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.
106106

107107
## Channels
108108

@@ -120,7 +120,7 @@ Eight tools, dot-notation names form a navigable tree (`post.*`, `channel.*`, `p
120120
## Example agent call
121121

122122
```jsonc
123-
// post.publish tool
123+
// post_publish tool
124124
{
125125
"content": {
126126
"id": "n8n-webhook-setup@2026-05-20",
@@ -153,18 +153,18 @@ Eight tools, dot-notation names form a navigable tree (`post.*`, `channel.*`, `p
153153

154154
## Idempotency
155155

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.
157157

158158
## Scheduling
159159

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:
161161

162162
```bash
163163
# fire due posts every 5 minutes
164164
*/5 * * * * npx -y content-distribution-mcp drain
165165
```
166166

167-
Or call the `post.drain` MCP tool directly from an agent.
167+
Or call the `post_drain` MCP tool directly from an agent.
168168

169169
## Environment variables
170170

src/server.ts

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@ const ContentSchema = z.object({
3030
});
3131

3232
const VariantSchema = z.object({
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."),
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."),
3434
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."),
3636
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."),
3737
canonical_url: z.string().url().optional().describe("Canonical URL override for this variant; overrides content.canonical_url for this channel only when set."),
3838
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."),
3939
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."),
4141
});
4242

4343
// --- Output schemas (raw shapes for registerTool) ----------------------------
@@ -65,7 +65,7 @@ const statusEntryShape = {
6565
live_url: z.string().nullable().describe("Public URL of the live post; null when not yet published."),
6666
published_at: z.string().nullable().describe("UTC ISO-8601 timestamp of the successful publish; null when not yet live."),
6767
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."),
6969
retry_count: z.number().nullable().describe("Number of publish attempts made so far; null for first attempt."),
7070
next_retry_at: z.string().nullable().describe("UTC ISO-8601 of the next scheduled retry; null when not queued for retry."),
7171
} as const;
@@ -134,16 +134,16 @@ export function createServer() {
134134
const adapters = buildAdapterMap();
135135
const backend = buildBackend();
136136

137-
// --- post.publish ---
137+
// --- post_publish ---
138138
server.registerTool(
139-
"post.publish",
139+
"post_publish",
140140
{
141141
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.",
143143
inputSchema: {
144144
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."),
147147
},
148148
outputSchema: publishOutputShape,
149149
annotations: {
@@ -161,16 +161,16 @@ export function createServer() {
161161
},
162162
);
163163

164-
// --- post.schedule ---
164+
// --- post_schedule ---
165165
server.registerTool(
166-
"post.schedule",
166+
"post_schedule",
167167
{
168168
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.",
170170
inputSchema: {
171171
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."),
174174
},
175175
outputSchema: scheduleOutputShape,
176176
annotations: {
@@ -188,12 +188,12 @@ export function createServer() {
188188
},
189189
);
190190

191-
// --- post.drain ---
191+
// --- post_drain ---
192192
server.registerTool(
193-
"post.drain",
193+
"post_drain",
194194
{
195195
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.",
197197
inputSchema: {
198198
now: z.string().optional().describe("ISO-8601 datetime boundary, e.g. '2026-05-21T09:00:00Z'; defaults to current UTC time when omitted."),
199199
},
@@ -212,12 +212,12 @@ export function createServer() {
212212
},
213213
);
214214

215-
// --- post.status ---
215+
// --- post_status ---
216216
server.registerTool(
217-
"post.status",
217+
"post_status",
218218
{
219219
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.",
221221
inputSchema: {
222222
content_id: z.string().optional().describe("Filter to a specific content piece by its stable ID; omit to return state for all content."),
223223
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() {
247247
},
248248
);
249249

250-
// --- post.unpublish ---
250+
// --- post_unpublish ---
251251
server.registerTool(
252-
"post.unpublish",
252+
"post_unpublish",
253253
{
254254
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.",
256256
inputSchema: {
257257
live_url: z.string().describe("URL of the live published post to retract, e.g. 'https://dev.to/user/post-slug'."),
258258
channel: z.string().describe("Channel slug the post was published to, e.g. 'devto', 'hashnode', 'reddit:ClaudeAI'."),
@@ -284,12 +284,12 @@ export function createServer() {
284284
},
285285
);
286286

287-
// --- channel.hints ---
287+
// --- channel_hints ---
288288
server.registerTool(
289-
"channel.hints",
289+
"channel_hints",
290290
{
291291
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.",
293293
inputSchema: {
294294
channel: z.string().describe("Channel platform name, e.g. 'devto', 'reddit', 'hashnode', 'bluesky'. Use the platform prefix only, not the full 'platform:account' form."),
295295
},
@@ -312,12 +312,12 @@ export function createServer() {
312312
},
313313
);
314314

315-
// --- profile.list ---
315+
// --- profile_list ---
316316
server.registerTool(
317-
"profile.list",
317+
"profile_list",
318318
{
319319
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.",
321321
inputSchema: {},
322322
outputSchema: profileListOutputShape,
323323
annotations: {
@@ -334,12 +334,12 @@ export function createServer() {
334334
},
335335
);
336336

337-
// --- subreddit.list ---
337+
// --- subreddit_list ---
338338
server.registerTool(
339-
"subreddit.list",
339+
"subreddit_list",
340340
{
341341
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.",
343343
inputSchema: {
344344
profile_name: z.string().optional().describe("Optional profile name to filter subreddits to those allowed by that profile; omit to return the full catalog."),
345345
},

0 commit comments

Comments
 (0)