posterly-mcp-server 0.50.2 → 0.50.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -123,7 +123,7 @@ Add the same server definition to your Cursor MCP settings:
123
123
 
124
124
  ## Available tools
125
125
 
126
- `posterly-mcp-server@0.50.2` exposes 64 tools covering 100 actions. The hosted HTTP endpoint exposes the same catalog minus the three signup tools: 61 tools covering 97 actions.
126
+ `posterly-mcp-server@0.50.4` exposes 64 tools covering 100 actions. The hosted HTTP endpoint exposes the same catalog minus the three signup tools: 61 tools covering 97 actions.
127
127
 
128
128
  Many tools pick what to do with one parameter: `view` (reads), `action` (writes), or `type`. A few pick their mode from which parameters you pass (for example `update_post` with `status` changes the status instead of editing). Every tool description names all of its views and actions.
129
129
 
@@ -532,3 +532,11 @@ The package reads:
532
532
  - optional `POSTERLY_API_KEY` for authenticated posterly tools
533
533
  - optional `POSTERLY_URL` if you need to point at a non-production environment
534
534
  - optional `POSTERLY_TOOLSETS` (for example `core,inbox`) to list fewer tool sets
535
+
536
+ ## Claude directory profile
537
+
538
+ The optional directory profile provides 52 tools over HTTP or 55 tools over local stdio for social publishing, existing-media uploads, analytics, and inbox work. AI image and video generation, generation-job helpers, billing mutations, and legacy tool aliases are unavailable in this profile. Text caption suggestions and read-only subscription information remain available. Tool descriptions are functional summaries; input schemas retain their validation constraints.
539
+
540
+ Hosted endpoint: `https://www.poster.ly/api/mcp?profile=claude`. For local stdio, set `POSTERLY_DIRECTORY_PROFILE=claude` alongside the existing API key. The default catalogs remain 61 hosted tools and 64 local tools. This profile does not grant authorization, bypass scopes, or change the existing preview and confirmation checks.
541
+
542
+ Directory approval remains Anthropic's decision. This profile addresses the generation and financial-transaction restrictions in the [Anthropic Software Directory Policy](https://support.claude.com/en/articles/13145358-anthropic-software-directory-policy).
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
5
5
  import { PosterlyClient, setMcpClientNameResolver } from './lib/api-client.js';
6
6
  import { POSTERLY_MCP_VERSION } from './lib/version.js';
7
7
  import { getToolAnnotations } from './lib/tool-annotations.js';
8
+ import { CLAUDE_DIRECTORY_INSTRUCTIONS, isClaudeDirectoryTool } from './lib/claude-directory-profile.js';
8
9
  import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
9
10
  import { AliasTransport } from './lib/alias-transport.js';
10
11
  import { StdioPreviewTokens } from './lib/preview-token.js';
@@ -148,6 +149,10 @@ const STDIO_TOOLS = [
148
149
  // tool-annotations.json; dist/index.js reads it from one level up.
149
150
  const require = createRequire(import.meta.url);
150
151
  const registry = require('../tool-registry.json');
152
+ const directoryProfile = process.env.POSTERLY_DIRECTORY_PROFILE;
153
+ if (directoryProfile && directoryProfile !== 'claude')
154
+ throw new Error('Unknown posterly directory profile');
155
+ const claudeDirectoryProfile = directoryProfile === 'claude';
151
156
  /**
152
157
  * Tool sets to list (POSTERLY_TOOLSETS env or --toolsets=core,inbox). Default
153
158
  * is every set, so nobody loses tools. Signup tools are always listed. Sets
@@ -169,7 +174,7 @@ const server = new McpServer({
169
174
  }, {
170
175
  // Which tool sets exist, that every set is on by default, how to load
171
176
  // fewer, and how preview then confirm works (built from the registry).
172
- instructions: toolsetInstructions(registry, 'stdio'),
177
+ instructions: claudeDirectoryProfile ? CLAUDE_DIRECTORY_INSTRUCTIONS : toolsetInstructions(registry, 'stdio'),
173
178
  });
174
179
  const client = new PosterlyClient();
175
180
  // The SDK only learns the connected client's identity (e.g. "claude-ai",
@@ -201,7 +206,7 @@ const legacyValidator = (aliasName, args) => {
201
206
  // Renamed tools (aliases) are never registered with McpServer: the transport
202
207
  // rewrites an alias call onto its target before the SDK sees it, and handlers
203
208
  // read the called name back through getCallInfo(extra.requestId).
204
- const transport = new AliasTransport(new StdioServerTransport(), registry, toolsets.all ? null : visibleTools(registry, toolsets.enabled, 'stdio'), legacyValidator);
209
+ const transport = new AliasTransport(new StdioServerTransport(), registry, toolsets.all ? null : visibleTools(registry, toolsets.enabled, 'stdio'), legacyValidator, claudeDirectoryProfile);
205
210
  // Preview then confirm tokens: a random key and an in-memory one-time-use
206
211
  // set per process (UX protection only; REST authorization is unchanged).
207
212
  const previews = new StdioPreviewTokens();
@@ -230,7 +235,7 @@ function registerAll(tools) {
230
235
  });
231
236
  }
232
237
  }
233
- registerAll(STDIO_TOOLS);
238
+ registerAll(claudeDirectoryProfile ? STDIO_TOOLS.filter((tool) => isClaudeDirectoryTool(tool.name)) : STDIO_TOOLS);
234
239
  // Start the server
235
240
  async function main() {
236
241
  await server.connect(transport);
@@ -58,6 +58,7 @@ export declare class AliasTransport implements Transport {
58
58
  private readonly registry;
59
59
  private readonly visibleTools;
60
60
  private readonly legacyValidator;
61
+ private readonly claudeDirectoryProfile;
61
62
  onclose?: () => void;
62
63
  onerror?: (error: Error) => void;
63
64
  onmessage?: <T extends JSONRPCMessage>(message: T, extra?: MessageExtraInfo) => void;
@@ -68,7 +69,7 @@ export declare class AliasTransport implements Transport {
68
69
  * @param visibleTools names to list in tools/list, or null to list every
69
70
  * registered tool unchanged (the default: every set).
70
71
  */
71
- constructor(inner: Transport, registry: ToolRegistry, visibleTools?: Set<string> | null, legacyValidator?: LegacyAliasValidator | null);
72
+ constructor(inner: Transport, registry: ToolRegistry, visibleTools?: Set<string> | null, legacyValidator?: LegacyAliasValidator | null, claudeDirectoryProfile?: boolean);
72
73
  get sessionId(): string | undefined;
73
74
  setProtocolVersion(version: string): void;
74
75
  start(): Promise<void>;
@@ -29,6 +29,7 @@
29
29
  *
30
30
  * Only the public Transport interface is used; no SDK private fields.
31
31
  */
32
+ import { claudeDirectoryDescription, claudeDirectorySchema, isClaudeDirectoryTool } from './claude-directory-profile.js';
32
33
  import { DEPRECATION_META_KEY, deprecationNote, resolveAlias, translateArgs, validateMode, } from './tool-registry.js';
33
34
  function requestKey(id) {
34
35
  return `${typeof id}:${String(id)}`;
@@ -41,6 +42,7 @@ export class AliasTransport {
41
42
  registry;
42
43
  visibleTools;
43
44
  legacyValidator;
45
+ claudeDirectoryProfile;
44
46
  onclose;
45
47
  onerror;
46
48
  onmessage;
@@ -51,11 +53,12 @@ export class AliasTransport {
51
53
  * @param visibleTools names to list in tools/list, or null to list every
52
54
  * registered tool unchanged (the default: every set).
53
55
  */
54
- constructor(inner, registry, visibleTools = null, legacyValidator = null) {
56
+ constructor(inner, registry, visibleTools = null, legacyValidator = null, claudeDirectoryProfile = false) {
55
57
  this.inner = inner;
56
58
  this.registry = registry;
57
59
  this.visibleTools = visibleTools;
58
60
  this.legacyValidator = legacyValidator;
61
+ this.claudeDirectoryProfile = claudeDirectoryProfile;
59
62
  inner.onmessage = (message, extra) => this.handleIncoming(message, extra);
60
63
  inner.onclose = () => this.onclose?.();
61
64
  inner.onerror = (error) => this.onerror?.(error);
@@ -95,7 +98,7 @@ export class AliasTransport {
95
98
  this.onmessage?.(message, extra);
96
99
  return;
97
100
  }
98
- if (request.method === 'tools/list' && this.visibleTools && request.id !== undefined && request.id !== null) {
101
+ if (request.method === 'tools/list' && (this.visibleTools || this.claudeDirectoryProfile) && request.id !== undefined && request.id !== null) {
99
102
  this.listRequests.add(requestKey(request.id));
100
103
  this.onmessage?.(message, extra);
101
104
  return;
@@ -105,6 +108,12 @@ export class AliasTransport {
105
108
  return;
106
109
  }
107
110
  const name = request.params.name;
111
+ if (this.claudeDirectoryProfile && (typeof name !== 'string' || !isClaudeDirectoryTool(name))) {
112
+ // Omitted tools and legacy aliases reach the SDK unchanged and fail as
113
+ // unregistered names, rather than being rewritten into callable tools.
114
+ this.onmessage?.(message, extra);
115
+ return;
116
+ }
108
117
  const alias = typeof name === 'string' ? resolveAlias(this.registry, name) : null;
109
118
  if (!alias) {
110
119
  this.onmessage?.(message, extra);
@@ -150,7 +159,7 @@ export class AliasTransport {
150
159
  }
151
160
  filterToolsList(message) {
152
161
  const response = message;
153
- if (!this.visibleTools || response.method !== undefined || response.id === undefined || response.id === null)
162
+ if ((!this.visibleTools && !this.claudeDirectoryProfile) || response.method !== undefined || response.id === undefined || response.id === null)
154
163
  return message;
155
164
  const key = requestKey(response.id);
156
165
  if (!this.listRequests.has(key))
@@ -163,7 +172,11 @@ export class AliasTransport {
163
172
  ...message,
164
173
  result: {
165
174
  ...response.result,
166
- tools: response.result.tools.filter((tool) => typeof tool?.name === 'string' && visible.has(tool.name)),
175
+ tools: response.result.tools
176
+ .filter((tool) => typeof tool?.name === 'string' && (!visible || visible.has(tool.name)) && (!this.claudeDirectoryProfile || isClaudeDirectoryTool(tool.name)))
177
+ .map((tool) => this.claudeDirectoryProfile ? {
178
+ ...tool, description: claudeDirectoryDescription(tool.name), inputSchema: claudeDirectorySchema(tool.inputSchema),
179
+ } : tool),
167
180
  },
168
181
  };
169
182
  return filtered;
@@ -0,0 +1,5 @@
1
+ export declare const CLAUDE_DIRECTORY_INSTRUCTIONS = "posterly provides social account discovery, existing-media uploads, social post validation and publication, analytics, and inbox management. AI image, video, and audio generation and billing mutations are unavailable in this directory profile. Public and destructive actions use the existing preview and confirmation contract.";
2
+ export declare function isClaudeDirectoryTool(name: string): boolean;
3
+ export declare function claudeDirectoryDescription(name: string): string;
4
+ /** Keep validation constraints but omit prose referring to other tools or model behavior. */
5
+ export declare function claudeDirectorySchema<T>(schema: T): T;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Opt-in catalog for the Anthropic Software Directory. Both MCP transports
3
+ * use this explicit allowlist; omitted tools are unavailable, including aliases.
4
+ * REST authorization and preview confirmation still apply to every operation.
5
+ * Policy: https://support.claude.com/en/articles/13145358
6
+ */
7
+ const descriptions = {
8
+ get_agent_signup_info: 'Return posterly signup requirements and the human signup link.',
9
+ start_signup: 'Create a short-lived signup session with a browser link for human completion. This operation does not take payment.',
10
+ get_signup_session: 'Read the completion status of a signup session.',
11
+ whoami: 'Return connection status or the authenticated user, scopes, and accessible workspaces.',
12
+ create_api_key: 'Create a scoped posterly API key for the authenticated user. The new key cannot exceed the caller key permissions.',
13
+ delete_api_key: 'Revoke an API key owned by the authenticated user. The current authentication key and managed OAuth keys cannot be revoked here.',
14
+ list_workspace_members: 'List workspace members, pending invites, roles, brand access, API access, and occupied seats.',
15
+ manage_workspace_member: 'Invite, update, or remove a workspace colleague using the action selector. Workspace administrator permissions and existing seat limits apply.',
16
+ list_publishing_pauses: 'Read workspace and brand publishing pauses and the caller permissions to manage them.',
17
+ manage_publishing_pause: 'Pause or resume workspace or brand publishing using the action selector. Resume can keep missed posts paused or publish them.',
18
+ list_accounts: 'List connected social accounts, or read account brand profiles, learned voice, performance profiles, and connection-session status using the view selector.',
19
+ disconnect_account: 'Disconnect a social account, removing its connection and affecting its scheduled posts.',
20
+ connect_account: 'Connect a credential-based Telegram, Bluesky, Discord, WordPress, Dev.to, Hashnode, or Lemmy account.',
21
+ create_connect_session: 'Create a short-lived browser handoff session for a human to connect a social account.',
22
+ list_oauth_clients: 'List self-serve OAuth developer clients owned by the authenticated user.',
23
+ manage_oauth_client: 'Create, update, or delete a public OAuth 2.1 developer client using the action selector. Client secrets are not created.',
24
+ list_platforms: 'List supported platforms, read platform or account settings schemas, or return browser connection links using the view selector.',
25
+ trigger_platform_helper: 'Read account-specific platform discovery data: Pinterest boards, TikTok creator information, LinkedIn recent mentions, or X quota.',
26
+ list_brands: 'List accessible brands, read one brand, or list its social accounts using the view selector.',
27
+ list_post_suggestions: 'List suggested social post drafts, their performance rationale, and their scheduling status.',
28
+ dismiss_suggestion: 'Dismiss a pending post suggestion. Suggestions already converted into posts are unchanged.',
29
+ list_posts: 'List social posts accessible to the caller, filtered by account, brand, workspace, platform, status, or approval state.',
30
+ get_post: 'Read one social post and its current content, media, platform settings, and publication status.',
31
+ ask_support: 'Answer a posterly support question with documentation and read-only workspace diagnostics. A continued support conversation can request a human ticket with explicit escalation confirmation.',
32
+ find_available_slot: 'Find available posting times with a one-hour spacing rule, preferred hours, and an explicit timezone.',
33
+ submit_agent_feedback: 'Record private operational feedback about a completed posterly workflow. Content and credentials are not accepted as telemetry.',
34
+ submit_product_feedback: 'Submit a confirmed title, category, and description to the public posterly feedback board.',
35
+ validate_post: 'Validate and normalize proposed post content without saving, uploading, scheduling, or publishing it.',
36
+ create_post: 'Create one scheduled or immediate social post. Omitting scheduled_at requests immediate publication. The operation creates content on the selected connected social account.',
37
+ create_posts_batch: 'Create one to 25 scheduled or immediate social posts. Partial success is possible and failed items retain their input index.',
38
+ update_post: 'Edit a draft or scheduled post, change its status, assign release and group identifiers, or manage client approval using the action selector. Published posts cannot be edited.',
39
+ delete_post: 'Delete one draft or scheduled post, or all eligible posts in a selected group using the action selector. Published posts cannot be deleted.',
40
+ get_x_posting_quota: 'Read managed X posting usage, quota, remaining posts, URL restrictions, and existing add-on information.',
41
+ upload_media: 'Upload an existing image or video to posterly storage from a file, base64 content, or a public HTTPS URL. Returns a reusable media URL and does not publish content.',
42
+ create_signed_upload: 'Create a signed storage upload URL for an existing large image or video.',
43
+ create_media_drop: 'Create a short-lived browser page for a human to upload existing media without a dashboard login.',
44
+ list_media: 'List uploaded media assets or files uploaded to a selected media-drop session.',
45
+ generate_captions: 'Generate or adapt text caption suggestions for social platforms. Uses the caption allowance and does not create or publish posts.',
46
+ list_analytics: 'Read account analytics, post analytics, post feedback insights, or missing-post requirements using the view selector.',
47
+ list_google_business_reviews: 'Read Google Business Profile reviews or the public review link using the view selector.',
48
+ audit_google_business_profile: 'Read and audit a connected Google Business Profile location: completeness, categories, media, attributes, reviews, and recommendations.',
49
+ manage_google_business_review: 'Suggest text replies, publish owner replies, or delete owner replies to Google Business reviews using the action selector. Suggestions do not publish a reply.',
50
+ list_google_business_media: 'Read existing photos and videos in connected Google Business Profile galleries.',
51
+ manage_google_business_media: 'Add an existing photo or video to a Google Business gallery, or delete a gallery item using the action selector.',
52
+ list_conversations: 'List Instagram or Facebook Page inbox conversations, or read one conversation using the view selector. Requires a Pro plan and posts:read.',
53
+ manage_conversation: 'Reply to an Instagram or Facebook Page inbox conversation, or synchronize supported social inbox data using the action selector. Requires a Pro plan and posts:write.',
54
+ list_comments: 'List Instagram, Facebook Page, Threads, or LinkedIn Page comments, or read one comment using the view selector. Requires a Pro plan and posts:read.',
55
+ manage_comment: 'Reply to, hide, unhide, mark, or delete a social inbox comment using the action selector. Platform limitations apply. Requires a Pro plan and posts:write.',
56
+ list_activity: 'Read recent post activity, status changes, publish attempts, failures, and retries.',
57
+ list_webhooks: 'Read webhook subscriptions, event filters, destinations, and recent delivery status.',
58
+ create_webhook: 'Create a webhook subscription for post, account, or analytics events and return its signing secret once.',
59
+ manage_webhook: 'Update or delete a webhook subscription, or send a signed test event using the action selector.',
60
+ get_credits: 'Read the workspace AI credit balance, included allowance, purchased balance, and next reset time. Does not buy or spend credits.',
61
+ get_subscription: 'Read subscription status, tier, current period, trial end, cancellation-at-period-end, and pause state. Does not change billing.',
62
+ get_updates: 'Read recent posterly product updates.',
63
+ };
64
+ export const CLAUDE_DIRECTORY_INSTRUCTIONS = 'posterly provides social account discovery, existing-media uploads, social post validation and publication, analytics, and inbox management. AI image, video, and audio generation and billing mutations are unavailable in this directory profile. Public and destructive actions use the existing preview and confirmation contract.';
65
+ export function isClaudeDirectoryTool(name) {
66
+ return Object.hasOwn(descriptions, name);
67
+ }
68
+ export function claudeDirectoryDescription(name) {
69
+ const description = descriptions[name];
70
+ if (!description)
71
+ throw new Error(`Tool is unavailable in the Claude directory profile: ${name}`);
72
+ return description;
73
+ }
74
+ /** Keep validation constraints but omit prose referring to other tools or model behavior. */
75
+ export function claudeDirectorySchema(schema) {
76
+ const copy = structuredClone(schema);
77
+ function visit(value) {
78
+ if (Array.isArray(value))
79
+ value.forEach(visit);
80
+ else if (value && typeof value === 'object') {
81
+ const object = value;
82
+ if (typeof object.description === 'string')
83
+ delete object.description;
84
+ for (const key of ['properties', 'patternProperties', 'definitions', '$defs', 'dependentSchemas']) {
85
+ const schemas = object[key];
86
+ if (schemas && typeof schemas === 'object' && !Array.isArray(schemas))
87
+ Object.values(schemas).forEach(visit);
88
+ }
89
+ for (const key of ['items', 'additionalProperties', 'additionalItems', 'contains', 'not', 'if', 'then', 'else', 'propertyNames', 'unevaluatedProperties', 'unevaluatedItems', 'allOf', 'oneOf', 'anyOf', 'prefixItems'])
90
+ visit(object[key]);
91
+ }
92
+ }
93
+ visit(copy);
94
+ return copy;
95
+ }
@@ -1 +1 @@
1
- export declare const POSTERLY_MCP_VERSION = "0.50.2";
1
+ export declare const POSTERLY_MCP_VERSION = "0.50.4";
@@ -5,4 +5,4 @@
5
5
  // tool set (minus the intentional pre-auth signup tools that only this stdio
6
6
  // package exposes). `npm run check:mcp-parity` enforces both the version match
7
7
  // and the tool-list match, and runs in the pre-commit hook.
8
- export const POSTERLY_MCP_VERSION = '0.50.2';
8
+ export const POSTERLY_MCP_VERSION = '0.50.4';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posterly-mcp-server",
3
- "version": "0.50.2",
3
+ "version": "0.50.4",
4
4
  "mcpName": "io.github.awpthorp/posterly",
5
5
  "description": "MCP server for posterly: schedule and publish social media posts across 18 platforms from any MCP client (Claude, ChatGPT, Cursor, Windsurf, Cline, and more)",
6
6
  "license": "MIT",
@@ -0,0 +1,38 @@
1
+ import assert from 'node:assert/strict';
2
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
3
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ async function connect(profile) {
7
+ const client = new Client({ name: 'directory-profile-test', version: '1.0.0' });
8
+ await client.connect(new StdioClientTransport({
9
+ command: process.execPath,
10
+ args: [fileURLToPath(new URL('../dist/index.js', import.meta.url))],
11
+ env: { PATH: process.env.PATH, POSTERLY_DIRECTORY_PROFILE: profile, POSTERLY_API_KEY: 'test', POSTERLY_API_URL: 'http://127.0.0.1:1/api/v1' },
12
+ }));
13
+ return client;
14
+ }
15
+
16
+ const client = await connect('claude');
17
+ try {
18
+ const { tools } = await client.listTools();
19
+ assert.equal(tools.length, 55);
20
+ for (const name of ['generate_image', 'generate_video', 'list_jobs', 'run_video_function', 'get_video_options', 'cancel_subscription', 'pause_subscription', 'resume_subscription', 'downgrade_subscription', 'get_image_job', 'get_video_job', 'update_post_status']) {
21
+ assert(!tools.some(tool => tool.name === name), name);
22
+ const rejected = await client.callTool({ name, arguments: {} });
23
+ assert.equal(rejected.isError, true, name);
24
+ assert.match(rejected.content.map(item => item.text ?? '').join('\n'), /not found|unknown tool/i, name);
25
+ }
26
+ for (const tool of tools) {
27
+ assert(tool.annotations.title, tool.name);
28
+ assert(!/call [a-z_]+|always |you must|walk |read .*first/i.test(tool.description), tool.name);
29
+ assert(! /"description"\s*:\s*"/.test(JSON.stringify(tool.inputSchema)), tool.name);
30
+ }
31
+ console.log('OK Claude directory: real stdio catalog has 55 tools and blocks generation, billing mutations, and legacy aliases.');
32
+ } finally { await client.close(); }
33
+
34
+ const standard = await connect('');
35
+ try {
36
+ assert.equal((await standard.listTools()).tools.length, 64);
37
+ console.log('OK Normal stdio catalog remains 64 tools.');
38
+ } finally { await standard.close(); }
package/server.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "io.github.awpthorp/posterly",
4
4
  "title": "posterly",
5
5
  "description": "Validate, schedule, publish, and analyze social content across 18 platforms with posterly.",
6
- "version": "0.50.2",
6
+ "version": "0.50.4",
7
7
  "websiteUrl": "https://www.poster.ly/mcp",
8
8
  "repository": {
9
9
  "url": "https://github.com/awpthorp/posterly",
@@ -14,7 +14,7 @@
14
14
  {
15
15
  "registryType": "npm",
16
16
  "identifier": "posterly-mcp-server",
17
- "version": "0.50.2",
17
+ "version": "0.50.4",
18
18
  "transport": {
19
19
  "type": "stdio"
20
20
  },
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ import type { z } from 'zod';
7
7
  import { PosterlyClient, setMcpClientNameResolver } from './lib/api-client.js';
8
8
  import { POSTERLY_MCP_VERSION } from './lib/version.js';
9
9
  import { getToolAnnotations } from './lib/tool-annotations.js';
10
+ import { CLAUDE_DIRECTORY_INSTRUCTIONS, isClaudeDirectoryTool } from './lib/claude-directory-profile.js';
10
11
  import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
11
12
  import { AliasTransport, type LegacyAliasValidator } from './lib/alias-transport.js';
12
13
  import { StdioPreviewTokens } from './lib/preview-token.js';
@@ -168,6 +169,9 @@ const STDIO_TOOLS: ToolModule[] = [
168
169
  // tool-annotations.json; dist/index.js reads it from one level up.
169
170
  const require = createRequire(import.meta.url);
170
171
  const registry = require('../tool-registry.json') as ToolRegistry;
172
+ const directoryProfile = process.env.POSTERLY_DIRECTORY_PROFILE;
173
+ if (directoryProfile && directoryProfile !== 'claude') throw new Error('Unknown posterly directory profile');
174
+ const claudeDirectoryProfile = directoryProfile === 'claude';
171
175
 
172
176
  /**
173
177
  * Tool sets to list (POSTERLY_TOOLSETS env or --toolsets=core,inbox). Default
@@ -193,7 +197,7 @@ const server = new McpServer(
193
197
  {
194
198
  // Which tool sets exist, that every set is on by default, how to load
195
199
  // fewer, and how preview then confirm works (built from the registry).
196
- instructions: toolsetInstructions(registry, 'stdio'),
200
+ instructions: claudeDirectoryProfile ? CLAUDE_DIRECTORY_INSTRUCTIONS : toolsetInstructions(registry, 'stdio'),
197
201
  },
198
202
  );
199
203
 
@@ -236,6 +240,7 @@ const transport = new AliasTransport(
236
240
  registry,
237
241
  toolsets.all ? null : visibleTools(registry, toolsets.enabled, 'stdio'),
238
242
  legacyValidator,
243
+ claudeDirectoryProfile,
239
244
  );
240
245
 
241
246
  // Preview then confirm tokens: a random key and an in-memory one-time-use
@@ -273,7 +278,7 @@ function registerAll(tools: ToolModule[]): void {
273
278
  }
274
279
  }
275
280
 
276
- registerAll(STDIO_TOOLS);
281
+ registerAll(claudeDirectoryProfile ? STDIO_TOOLS.filter((tool) => isClaudeDirectoryTool(tool.name)) : STDIO_TOOLS);
277
282
 
278
283
  // Start the server
279
284
  async function main() {
@@ -32,6 +32,7 @@
32
32
 
33
33
  import type { Transport, TransportSendOptions } from '@modelcontextprotocol/sdk/shared/transport.js';
34
34
  import type { JSONRPCMessage, MessageExtraInfo, RequestId } from '@modelcontextprotocol/sdk/types.js';
35
+ import { claudeDirectoryDescription, claudeDirectorySchema, isClaudeDirectoryTool } from './claude-directory-profile.js';
35
36
  import {
36
37
  DEPRECATION_META_KEY,
37
38
  deprecationNote,
@@ -88,6 +89,7 @@ export class AliasTransport implements Transport {
88
89
  private readonly registry: ToolRegistry,
89
90
  private readonly visibleTools: Set<string> | null = null,
90
91
  private readonly legacyValidator: LegacyAliasValidator | null = null,
92
+ private readonly claudeDirectoryProfile = false,
91
93
  ) {
92
94
  inner.onmessage = (message, extra) => this.handleIncoming(message as JSONRPCMessage, extra);
93
95
  inner.onclose = () => this.onclose?.();
@@ -134,7 +136,7 @@ export class AliasTransport implements Transport {
134
136
  this.onmessage?.(message, extra);
135
137
  return;
136
138
  }
137
- if (request.method === 'tools/list' && this.visibleTools && request.id !== undefined && request.id !== null) {
139
+ if (request.method === 'tools/list' && (this.visibleTools || this.claudeDirectoryProfile) && request.id !== undefined && request.id !== null) {
138
140
  this.listRequests.add(requestKey(request.id));
139
141
  this.onmessage?.(message, extra);
140
142
  return;
@@ -144,6 +146,12 @@ export class AliasTransport implements Transport {
144
146
  return;
145
147
  }
146
148
  const name = request.params.name;
149
+ if (this.claudeDirectoryProfile && (typeof name !== 'string' || !isClaudeDirectoryTool(name))) {
150
+ // Omitted tools and legacy aliases reach the SDK unchanged and fail as
151
+ // unregistered names, rather than being rewritten into callable tools.
152
+ this.onmessage?.(message, extra);
153
+ return;
154
+ }
147
155
  const alias = typeof name === 'string' ? resolveAlias(this.registry, name) : null;
148
156
  if (!alias) {
149
157
  this.onmessage?.(message, extra);
@@ -194,7 +202,7 @@ export class AliasTransport implements Transport {
194
202
 
195
203
  private filterToolsList(message: JSONRPCMessage): JSONRPCMessage {
196
204
  const response = message as { id?: RequestId; result?: any; method?: string };
197
- if (!this.visibleTools || response.method !== undefined || response.id === undefined || response.id === null) return message;
205
+ if ((!this.visibleTools && !this.claudeDirectoryProfile) || response.method !== undefined || response.id === undefined || response.id === null) return message;
198
206
  const key = requestKey(response.id);
199
207
  if (!this.listRequests.has(key)) return message;
200
208
  this.listRequests.delete(key);
@@ -204,7 +212,11 @@ export class AliasTransport implements Transport {
204
212
  ...(message as object),
205
213
  result: {
206
214
  ...response.result,
207
- tools: response.result.tools.filter((tool: { name?: unknown }) => typeof tool?.name === 'string' && visible.has(tool.name)),
215
+ tools: response.result.tools
216
+ .filter((tool: { name?: unknown }) => typeof tool?.name === 'string' && (!visible || visible.has(tool.name)) && (!this.claudeDirectoryProfile || isClaudeDirectoryTool(tool.name)))
217
+ .map((tool: { name: string; inputSchema: unknown }) => this.claudeDirectoryProfile ? {
218
+ ...tool, description: claudeDirectoryDescription(tool.name), inputSchema: claudeDirectorySchema(tool.inputSchema),
219
+ } : tool),
208
220
  },
209
221
  };
210
222
  return filtered as unknown as JSONRPCMessage;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Opt-in catalog for the Anthropic Software Directory. Both MCP transports
3
+ * use this explicit allowlist; omitted tools are unavailable, including aliases.
4
+ * REST authorization and preview confirmation still apply to every operation.
5
+ * Policy: https://support.claude.com/en/articles/13145358
6
+ */
7
+ const descriptions: Record<string, string> = {
8
+ get_agent_signup_info: 'Return posterly signup requirements and the human signup link.',
9
+ start_signup: 'Create a short-lived signup session with a browser link for human completion. This operation does not take payment.',
10
+ get_signup_session: 'Read the completion status of a signup session.',
11
+ whoami: 'Return connection status or the authenticated user, scopes, and accessible workspaces.',
12
+ create_api_key: 'Create a scoped posterly API key for the authenticated user. The new key cannot exceed the caller key permissions.',
13
+ delete_api_key: 'Revoke an API key owned by the authenticated user. The current authentication key and managed OAuth keys cannot be revoked here.',
14
+ list_workspace_members: 'List workspace members, pending invites, roles, brand access, API access, and occupied seats.',
15
+ manage_workspace_member: 'Invite, update, or remove a workspace colleague using the action selector. Workspace administrator permissions and existing seat limits apply.',
16
+ list_publishing_pauses: 'Read workspace and brand publishing pauses and the caller permissions to manage them.',
17
+ manage_publishing_pause: 'Pause or resume workspace or brand publishing using the action selector. Resume can keep missed posts paused or publish them.',
18
+ list_accounts: 'List connected social accounts, or read account brand profiles, learned voice, performance profiles, and connection-session status using the view selector.',
19
+ disconnect_account: 'Disconnect a social account, removing its connection and affecting its scheduled posts.',
20
+ connect_account: 'Connect a credential-based Telegram, Bluesky, Discord, WordPress, Dev.to, Hashnode, or Lemmy account.',
21
+ create_connect_session: 'Create a short-lived browser handoff session for a human to connect a social account.',
22
+ list_oauth_clients: 'List self-serve OAuth developer clients owned by the authenticated user.',
23
+ manage_oauth_client: 'Create, update, or delete a public OAuth 2.1 developer client using the action selector. Client secrets are not created.',
24
+ list_platforms: 'List supported platforms, read platform or account settings schemas, or return browser connection links using the view selector.',
25
+ trigger_platform_helper: 'Read account-specific platform discovery data: Pinterest boards, TikTok creator information, LinkedIn recent mentions, or X quota.',
26
+ list_brands: 'List accessible brands, read one brand, or list its social accounts using the view selector.',
27
+ list_post_suggestions: 'List suggested social post drafts, their performance rationale, and their scheduling status.',
28
+ dismiss_suggestion: 'Dismiss a pending post suggestion. Suggestions already converted into posts are unchanged.',
29
+ list_posts: 'List social posts accessible to the caller, filtered by account, brand, workspace, platform, status, or approval state.',
30
+ get_post: 'Read one social post and its current content, media, platform settings, and publication status.',
31
+ ask_support: 'Answer a posterly support question with documentation and read-only workspace diagnostics. A continued support conversation can request a human ticket with explicit escalation confirmation.',
32
+ find_available_slot: 'Find available posting times with a one-hour spacing rule, preferred hours, and an explicit timezone.',
33
+ submit_agent_feedback: 'Record private operational feedback about a completed posterly workflow. Content and credentials are not accepted as telemetry.',
34
+ submit_product_feedback: 'Submit a confirmed title, category, and description to the public posterly feedback board.',
35
+ validate_post: 'Validate and normalize proposed post content without saving, uploading, scheduling, or publishing it.',
36
+ create_post: 'Create one scheduled or immediate social post. Omitting scheduled_at requests immediate publication. The operation creates content on the selected connected social account.',
37
+ create_posts_batch: 'Create one to 25 scheduled or immediate social posts. Partial success is possible and failed items retain their input index.',
38
+ update_post: 'Edit a draft or scheduled post, change its status, assign release and group identifiers, or manage client approval using the action selector. Published posts cannot be edited.',
39
+ delete_post: 'Delete one draft or scheduled post, or all eligible posts in a selected group using the action selector. Published posts cannot be deleted.',
40
+ get_x_posting_quota: 'Read managed X posting usage, quota, remaining posts, URL restrictions, and existing add-on information.',
41
+ upload_media: 'Upload an existing image or video to posterly storage from a file, base64 content, or a public HTTPS URL. Returns a reusable media URL and does not publish content.',
42
+ create_signed_upload: 'Create a signed storage upload URL for an existing large image or video.',
43
+ create_media_drop: 'Create a short-lived browser page for a human to upload existing media without a dashboard login.',
44
+ list_media: 'List uploaded media assets or files uploaded to a selected media-drop session.',
45
+ generate_captions: 'Generate or adapt text caption suggestions for social platforms. Uses the caption allowance and does not create or publish posts.',
46
+ list_analytics: 'Read account analytics, post analytics, post feedback insights, or missing-post requirements using the view selector.',
47
+ list_google_business_reviews: 'Read Google Business Profile reviews or the public review link using the view selector.',
48
+ audit_google_business_profile: 'Read and audit a connected Google Business Profile location: completeness, categories, media, attributes, reviews, and recommendations.',
49
+ manage_google_business_review: 'Suggest text replies, publish owner replies, or delete owner replies to Google Business reviews using the action selector. Suggestions do not publish a reply.',
50
+ list_google_business_media: 'Read existing photos and videos in connected Google Business Profile galleries.',
51
+ manage_google_business_media: 'Add an existing photo or video to a Google Business gallery, or delete a gallery item using the action selector.',
52
+ list_conversations: 'List Instagram or Facebook Page inbox conversations, or read one conversation using the view selector. Requires a Pro plan and posts:read.',
53
+ manage_conversation: 'Reply to an Instagram or Facebook Page inbox conversation, or synchronize supported social inbox data using the action selector. Requires a Pro plan and posts:write.',
54
+ list_comments: 'List Instagram, Facebook Page, Threads, or LinkedIn Page comments, or read one comment using the view selector. Requires a Pro plan and posts:read.',
55
+ manage_comment: 'Reply to, hide, unhide, mark, or delete a social inbox comment using the action selector. Platform limitations apply. Requires a Pro plan and posts:write.',
56
+ list_activity: 'Read recent post activity, status changes, publish attempts, failures, and retries.',
57
+ list_webhooks: 'Read webhook subscriptions, event filters, destinations, and recent delivery status.',
58
+ create_webhook: 'Create a webhook subscription for post, account, or analytics events and return its signing secret once.',
59
+ manage_webhook: 'Update or delete a webhook subscription, or send a signed test event using the action selector.',
60
+ get_credits: 'Read the workspace AI credit balance, included allowance, purchased balance, and next reset time. Does not buy or spend credits.',
61
+ get_subscription: 'Read subscription status, tier, current period, trial end, cancellation-at-period-end, and pause state. Does not change billing.',
62
+ get_updates: 'Read recent posterly product updates.',
63
+ }
64
+
65
+ export const CLAUDE_DIRECTORY_INSTRUCTIONS = 'posterly provides social account discovery, existing-media uploads, social post validation and publication, analytics, and inbox management. AI image, video, and audio generation and billing mutations are unavailable in this directory profile. Public and destructive actions use the existing preview and confirmation contract.'
66
+
67
+ export function isClaudeDirectoryTool(name: string): boolean {
68
+ return Object.hasOwn(descriptions, name)
69
+ }
70
+
71
+ export function claudeDirectoryDescription(name: string): string {
72
+ const description = descriptions[name]
73
+ if (!description) throw new Error(`Tool is unavailable in the Claude directory profile: ${name}`)
74
+ return description
75
+ }
76
+
77
+ /** Keep validation constraints but omit prose referring to other tools or model behavior. */
78
+ export function claudeDirectorySchema<T>(schema: T): T {
79
+ const copy = structuredClone(schema)
80
+ function visit(value: unknown): void {
81
+ if (Array.isArray(value)) value.forEach(visit)
82
+ else if (value && typeof value === 'object') {
83
+ const object = value as Record<string, unknown>
84
+ if (typeof object.description === 'string') delete object.description
85
+ for (const key of ['properties', 'patternProperties', 'definitions', '$defs', 'dependentSchemas']) {
86
+ const schemas = object[key]
87
+ if (schemas && typeof schemas === 'object' && !Array.isArray(schemas)) Object.values(schemas).forEach(visit)
88
+ }
89
+ for (const key of ['items', 'additionalProperties', 'additionalItems', 'contains', 'not', 'if', 'then', 'else', 'propertyNames', 'unevaluatedProperties', 'unevaluatedItems', 'allOf', 'oneOf', 'anyOf', 'prefixItems']) visit(object[key])
90
+ }
91
+ }
92
+ visit(copy)
93
+ return copy
94
+ }
@@ -5,4 +5,4 @@
5
5
  // tool set (minus the intentional pre-auth signup tools that only this stdio
6
6
  // package exposes). `npm run check:mcp-parity` enforces both the version match
7
7
  // and the tool-list match, and runs in the pre-commit hook.
8
- export const POSTERLY_MCP_VERSION = '0.50.2';
8
+ export const POSTERLY_MCP_VERSION = '0.50.4';
@@ -855,7 +855,7 @@
855
855
  "label": "Pause, resume, schedule, or move to draft",
856
856
  "destructive": true,
857
857
  "idempotent": true,
858
- "openWorld": false,
858
+ "openWorld": true,
859
859
  "confirm": "confirm",
860
860
  "when": [
861
861
  "status"