posterly-mcp-server 0.43.8 → 0.45.0

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.
Files changed (169) hide show
  1. package/README.md +310 -109
  2. package/dist/index.js +190 -874
  3. package/dist/lib/alias-transport.d.ts +84 -0
  4. package/dist/lib/alias-transport.js +193 -0
  5. package/dist/lib/api-client.d.ts +97 -0
  6. package/dist/lib/api-client.js +81 -0
  7. package/dist/lib/preview-token-core.d.ts +62 -0
  8. package/dist/lib/preview-token-core.js +131 -0
  9. package/dist/lib/preview-token.d.ts +23 -0
  10. package/dist/lib/preview-token.js +44 -0
  11. package/dist/lib/tool-registry.d.ts +246 -0
  12. package/dist/lib/tool-registry.js +394 -0
  13. package/dist/lib/tool-runtime.d.ts +68 -0
  14. package/dist/lib/tool-runtime.js +85 -0
  15. package/dist/lib/version.d.ts +1 -1
  16. package/dist/lib/version.js +1 -1
  17. package/dist/tools/audit-google-business-profile.js +1 -1
  18. package/dist/tools/connect-account.d.ts +4 -4
  19. package/dist/tools/connect-account.js +2 -2
  20. package/dist/tools/create-api-key.js +1 -1
  21. package/dist/tools/create-connect-session.d.ts +2 -2
  22. package/dist/tools/create-connect-session.js +2 -2
  23. package/dist/tools/create-post.d.ts +6 -6
  24. package/dist/tools/create-post.js +3 -3
  25. package/dist/tools/create-posts-batch.d.ts +4 -4
  26. package/dist/tools/create-webhook.d.ts +4 -4
  27. package/dist/tools/delete-post.d.ts +34 -10
  28. package/dist/tools/delete-post.js +52 -16
  29. package/dist/tools/disconnect-account.d.ts +2 -2
  30. package/dist/tools/generate-captions.d.ts +2 -2
  31. package/dist/tools/generate-image.d.ts +4 -4
  32. package/dist/tools/generate-image.js +2 -2
  33. package/dist/tools/generate-video.d.ts +6 -6
  34. package/dist/tools/generate-video.js +1 -1
  35. package/dist/tools/get-agent-signup-info.js +2 -2
  36. package/dist/tools/get-post.d.ts +16 -1
  37. package/dist/tools/get-post.js +35 -19
  38. package/dist/tools/list-accounts.d.ts +49 -3
  39. package/dist/tools/list-accounts.js +148 -33
  40. package/dist/tools/list-analytics.d.ts +111 -0
  41. package/dist/tools/list-analytics.js +393 -0
  42. package/dist/tools/list-brands.d.ts +38 -3
  43. package/dist/tools/list-brands.js +138 -20
  44. package/dist/tools/list-comments.d.ts +29 -9
  45. package/dist/tools/list-comments.js +75 -34
  46. package/dist/tools/list-conversations.d.ts +32 -9
  47. package/dist/tools/list-conversations.js +63 -27
  48. package/dist/tools/list-google-business-media.js +1 -1
  49. package/dist/tools/list-google-business-reviews.d.ts +32 -9
  50. package/dist/tools/list-google-business-reviews.js +77 -52
  51. package/dist/tools/list-jobs.d.ts +60 -0
  52. package/dist/tools/list-jobs.js +81 -0
  53. package/dist/tools/list-platforms.d.ts +62 -3
  54. package/dist/tools/list-platforms.js +118 -9
  55. package/dist/tools/list-posts.d.ts +2 -2
  56. package/dist/tools/list-workspace-members.d.ts +17 -0
  57. package/dist/tools/list-workspace-members.js +51 -0
  58. package/dist/tools/manage-comment.d.ts +102 -0
  59. package/dist/tools/manage-comment.js +125 -0
  60. package/dist/tools/manage-conversation.d.ts +79 -0
  61. package/dist/tools/manage-conversation.js +75 -0
  62. package/dist/tools/manage-google-business-media.d.ts +103 -0
  63. package/dist/tools/manage-google-business-media.js +126 -0
  64. package/dist/tools/manage-google-business-review.d.ts +146 -0
  65. package/dist/tools/manage-google-business-review.js +115 -0
  66. package/dist/tools/manage-oauth-client.d.ts +121 -0
  67. package/dist/tools/manage-oauth-client.js +94 -0
  68. package/dist/tools/manage-webhook.d.ts +99 -0
  69. package/dist/tools/manage-webhook.js +87 -0
  70. package/dist/tools/manage-workspace-member.d.ts +57 -0
  71. package/dist/tools/manage-workspace-member.js +131 -0
  72. package/dist/tools/reply-to-comment.d.ts +2 -2
  73. package/dist/tools/send-message.d.ts +2 -2
  74. package/dist/tools/start-signup.d.ts +2 -2
  75. package/dist/tools/submit-agent-feedback.d.ts +4 -4
  76. package/dist/tools/submit-product-feedback.d.ts +4 -4
  77. package/dist/tools/trigger-platform-helper.d.ts +2 -2
  78. package/dist/tools/update-post-release-id.d.ts +2 -2
  79. package/dist/tools/update-post-status.d.ts +2 -2
  80. package/dist/tools/update-post.d.ts +64 -13
  81. package/dist/tools/update-post.js +79 -26
  82. package/dist/tools/upload-media.d.ts +32 -9
  83. package/dist/tools/upload-media.js +54 -18
  84. package/dist/tools/validate-post.d.ts +4 -4
  85. package/dist/tools/whoami.d.ts +15 -2
  86. package/dist/tools/whoami.js +221 -25
  87. package/package.json +5 -3
  88. package/scripts/test-alias-transport.mjs +368 -0
  89. package/scripts/test-client-header-sanitization.mjs +78 -0
  90. package/scripts/test-tool-annotations.mjs +38 -17
  91. package/server.json +9 -2
  92. package/src/index.ts +227 -1411
  93. package/src/lib/alias-transport.ts +233 -0
  94. package/src/lib/api-client.ts +168 -0
  95. package/src/lib/preview-token-core.ts +157 -0
  96. package/src/lib/preview-token.ts +54 -0
  97. package/src/lib/tool-registry.ts +538 -0
  98. package/src/lib/tool-runtime.ts +117 -0
  99. package/src/lib/version.ts +1 -1
  100. package/src/tools/audit-google-business-profile.ts +1 -1
  101. package/src/tools/connect-account.ts +2 -2
  102. package/src/tools/create-api-key.ts +1 -1
  103. package/src/tools/create-connect-session.ts +2 -2
  104. package/src/tools/create-post.ts +3 -3
  105. package/src/tools/delete-post.ts +60 -19
  106. package/src/tools/generate-image.ts +2 -2
  107. package/src/tools/generate-video.ts +1 -1
  108. package/src/tools/get-agent-signup-info.ts +2 -2
  109. package/src/tools/get-post.ts +35 -21
  110. package/src/tools/list-accounts.ts +187 -39
  111. package/src/tools/list-analytics.ts +444 -0
  112. package/src/tools/list-brands.ts +162 -26
  113. package/src/tools/list-comments.ts +86 -43
  114. package/src/tools/list-conversations.ts +81 -39
  115. package/src/tools/list-google-business-media.ts +1 -1
  116. package/src/tools/list-google-business-reviews.ts +93 -64
  117. package/src/tools/list-jobs.ts +91 -0
  118. package/src/tools/list-platforms.ts +135 -11
  119. package/src/tools/list-workspace-members.ts +61 -0
  120. package/src/tools/manage-comment.ts +142 -0
  121. package/src/tools/manage-conversation.ts +89 -0
  122. package/src/tools/manage-google-business-media.ts +147 -0
  123. package/src/tools/manage-google-business-review.ts +139 -0
  124. package/src/tools/manage-oauth-client.ts +116 -0
  125. package/src/tools/manage-webhook.ts +103 -0
  126. package/src/tools/manage-workspace-member.ts +151 -0
  127. package/src/tools/update-post.ts +106 -39
  128. package/src/tools/upload-media.ts +76 -28
  129. package/src/tools/whoami.ts +253 -30
  130. package/tool-annotations.json +76 -144
  131. package/tool-registry.json +2085 -0
  132. package/src/tools/add-google-business-media.ts +0 -50
  133. package/src/tools/create-oauth-client.ts +0 -36
  134. package/src/tools/delete-comment.ts +0 -29
  135. package/src/tools/delete-google-business-media.ts +0 -30
  136. package/src/tools/delete-google-business-review-reply.ts +0 -30
  137. package/src/tools/delete-oauth-client.ts +0 -23
  138. package/src/tools/delete-post-group.ts +0 -17
  139. package/src/tools/delete-webhook.ts +0 -17
  140. package/src/tools/get-account-analytics.ts +0 -171
  141. package/src/tools/get-brand-profile.ts +0 -86
  142. package/src/tools/get-brand.ts +0 -34
  143. package/src/tools/get-comment.ts +0 -47
  144. package/src/tools/get-connect-link.ts +0 -61
  145. package/src/tools/get-connect-session.ts +0 -63
  146. package/src/tools/get-conversation.ts +0 -44
  147. package/src/tools/get-google-business-review-link.ts +0 -30
  148. package/src/tools/get-image-job.ts +0 -36
  149. package/src/tools/get-learned-voice.ts +0 -31
  150. package/src/tools/get-mcp-status.ts +0 -212
  151. package/src/tools/get-performance-profile.ts +0 -47
  152. package/src/tools/get-platform-schema.ts +0 -56
  153. package/src/tools/get-post-analytics.ts +0 -243
  154. package/src/tools/get-post-insights.ts +0 -60
  155. package/src/tools/get-post-missing.ts +0 -16
  156. package/src/tools/get-video-job.ts +0 -36
  157. package/src/tools/list-brand-accounts.ts +0 -36
  158. package/src/tools/reply-google-business-review.ts +0 -32
  159. package/src/tools/reply-to-comment.ts +0 -30
  160. package/src/tools/send-message.ts +0 -28
  161. package/src/tools/suggest-google-business-review-reply.ts +0 -44
  162. package/src/tools/sync-inbox.ts +0 -28
  163. package/src/tools/test-webhook.ts +0 -18
  164. package/src/tools/update-comment.ts +0 -31
  165. package/src/tools/update-oauth-client.ts +0 -38
  166. package/src/tools/update-post-release-id.ts +0 -25
  167. package/src/tools/update-post-status.ts +0 -31
  168. package/src/tools/update-webhook.ts +0 -36
  169. package/src/tools/upload-media-from-url.ts +0 -33
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Transport wrapper that keeps renamed MCP tools working on the stdio package
3
+ * without registering the old names with McpServer (so they never appear in
4
+ * tools/list).
5
+ *
6
+ * Incoming: a tools/call whose name is an alias is rewritten BEFORE McpServer
7
+ * sees it: params.name becomes the target tool and params.arguments become
8
+ * the translated arguments, so the SDK validates them against the canonical
9
+ * schema. The request id is remembered with the alias details.
10
+ *
11
+ * Handlers read that per-request info through getCallInfo(extra.requestId):
12
+ * the called name (for the X-Posterly-Tool telemetry header) and whether the
13
+ * alias may still confirm in one step. Nothing is stored in the arguments.
14
+ *
15
+ * Outgoing: the response for that id gets the rename note APPENDED as a final
16
+ * text item (content[0] is never touched) plus _meta["posterly/deprecation"],
17
+ * and the remembered entry is deleted. A notifications/cancelled for that id
18
+ * also deletes it (the SDK sends no response for a cancelled request).
19
+ *
20
+ * Legacy input schemas: when a legacyValidator is given, an alias call's
21
+ * arguments are first checked against the OLD tool's exact input schema, so a
22
+ * call the old tool rejected gets the same SDK validation error text (and a
23
+ * call it accepted gets the same parsed arguments, defaults included) before
24
+ * translation. The npm package passes the old zod schemas here.
25
+ *
26
+ * Tool sets: every tool stays registered (so a call to any tool always
27
+ * works); when a visible set is given, only the outgoing tools/list result is
28
+ * filtered to it.
29
+ *
30
+ * Only the public Transport interface is used; no SDK private fields.
31
+ */
32
+ import type { Transport, TransportSendOptions } from '@modelcontextprotocol/sdk/shared/transport.js';
33
+ import type { JSONRPCMessage, MessageExtraInfo, RequestId } from '@modelcontextprotocol/sdk/types.js';
34
+ import { type ResolvedAlias, type ToolRegistry } from './tool-registry.js';
35
+ export interface AliasCallInfo {
36
+ /** The name the client called (the alias). */
37
+ calledName: string;
38
+ /** The canonical tool the call was routed to. */
39
+ target: string;
40
+ /** The alias may confirm with confirm:true alone until its sunset date. */
41
+ legacyConfirm: boolean;
42
+ alias: ResolvedAlias;
43
+ }
44
+ /**
45
+ * Checks an alias call's raw arguments against the old tool's input schema.
46
+ * Return null when the alias has no legacy schema (no check), the parsed
47
+ * arguments when they are valid, or the exact error text the old tool gave.
48
+ */
49
+ export type LegacyAliasValidator = (aliasName: string, args: unknown) => {
50
+ ok: true;
51
+ args: Record<string, unknown>;
52
+ } | {
53
+ ok: false;
54
+ message: string;
55
+ } | null;
56
+ export declare class AliasTransport implements Transport {
57
+ private readonly inner;
58
+ private readonly registry;
59
+ private readonly visibleTools;
60
+ private readonly legacyValidator;
61
+ onclose?: () => void;
62
+ onerror?: (error: Error) => void;
63
+ onmessage?: <T extends JSONRPCMessage>(message: T, extra?: MessageExtraInfo) => void;
64
+ private readonly calls;
65
+ /** In-flight tools/list request ids, so only their responses are filtered. */
66
+ private readonly listRequests;
67
+ /**
68
+ * @param visibleTools names to list in tools/list, or null to list every
69
+ * registered tool unchanged (the default: every set).
70
+ */
71
+ constructor(inner: Transport, registry: ToolRegistry, visibleTools?: Set<string> | null, legacyValidator?: LegacyAliasValidator | null);
72
+ get sessionId(): string | undefined;
73
+ setProtocolVersion(version: string): void;
74
+ start(): Promise<void>;
75
+ close(): Promise<void>;
76
+ /** Alias details for an in-flight tools/call, or undefined for a canonical call. */
77
+ getCallInfo(requestId: RequestId | undefined): AliasCallInfo | undefined;
78
+ send(message: JSONRPCMessage, options?: TransportSendOptions): Promise<void>;
79
+ private handleIncoming;
80
+ /** Answers an alias call directly with a plain isError result; send() appends the rename note. */
81
+ private respondWithError;
82
+ private filterToolsList;
83
+ private decorateResponse;
84
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Transport wrapper that keeps renamed MCP tools working on the stdio package
3
+ * without registering the old names with McpServer (so they never appear in
4
+ * tools/list).
5
+ *
6
+ * Incoming: a tools/call whose name is an alias is rewritten BEFORE McpServer
7
+ * sees it: params.name becomes the target tool and params.arguments become
8
+ * the translated arguments, so the SDK validates them against the canonical
9
+ * schema. The request id is remembered with the alias details.
10
+ *
11
+ * Handlers read that per-request info through getCallInfo(extra.requestId):
12
+ * the called name (for the X-Posterly-Tool telemetry header) and whether the
13
+ * alias may still confirm in one step. Nothing is stored in the arguments.
14
+ *
15
+ * Outgoing: the response for that id gets the rename note APPENDED as a final
16
+ * text item (content[0] is never touched) plus _meta["posterly/deprecation"],
17
+ * and the remembered entry is deleted. A notifications/cancelled for that id
18
+ * also deletes it (the SDK sends no response for a cancelled request).
19
+ *
20
+ * Legacy input schemas: when a legacyValidator is given, an alias call's
21
+ * arguments are first checked against the OLD tool's exact input schema, so a
22
+ * call the old tool rejected gets the same SDK validation error text (and a
23
+ * call it accepted gets the same parsed arguments, defaults included) before
24
+ * translation. The npm package passes the old zod schemas here.
25
+ *
26
+ * Tool sets: every tool stays registered (so a call to any tool always
27
+ * works); when a visible set is given, only the outgoing tools/list result is
28
+ * filtered to it.
29
+ *
30
+ * Only the public Transport interface is used; no SDK private fields.
31
+ */
32
+ import { DEPRECATION_META_KEY, deprecationNote, resolveAlias, translateArgs, validateMode, } from './tool-registry.js';
33
+ function requestKey(id) {
34
+ return `${typeof id}:${String(id)}`;
35
+ }
36
+ function isObject(value) {
37
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
38
+ }
39
+ export class AliasTransport {
40
+ inner;
41
+ registry;
42
+ visibleTools;
43
+ legacyValidator;
44
+ onclose;
45
+ onerror;
46
+ onmessage;
47
+ calls = new Map();
48
+ /** In-flight tools/list request ids, so only their responses are filtered. */
49
+ listRequests = new Set();
50
+ /**
51
+ * @param visibleTools names to list in tools/list, or null to list every
52
+ * registered tool unchanged (the default: every set).
53
+ */
54
+ constructor(inner, registry, visibleTools = null, legacyValidator = null) {
55
+ this.inner = inner;
56
+ this.registry = registry;
57
+ this.visibleTools = visibleTools;
58
+ this.legacyValidator = legacyValidator;
59
+ inner.onmessage = (message, extra) => this.handleIncoming(message, extra);
60
+ inner.onclose = () => this.onclose?.();
61
+ inner.onerror = (error) => this.onerror?.(error);
62
+ }
63
+ get sessionId() {
64
+ return this.inner.sessionId;
65
+ }
66
+ setProtocolVersion(version) {
67
+ this.inner.setProtocolVersion?.(version);
68
+ }
69
+ start() {
70
+ return this.inner.start();
71
+ }
72
+ close() {
73
+ this.calls.clear();
74
+ this.listRequests.clear();
75
+ return this.inner.close();
76
+ }
77
+ /** Alias details for an in-flight tools/call, or undefined for a canonical call. */
78
+ getCallInfo(requestId) {
79
+ if (requestId === undefined || requestId === null)
80
+ return undefined;
81
+ return this.calls.get(requestKey(requestId));
82
+ }
83
+ async send(message, options) {
84
+ const outgoing = this.filterToolsList(this.decorateResponse(message));
85
+ return this.inner.send(outgoing, options);
86
+ }
87
+ handleIncoming(message, extra) {
88
+ const request = message;
89
+ if (request.method === 'notifications/cancelled') {
90
+ const cancelled = isObject(request.params) ? request.params.requestId : undefined;
91
+ if (typeof cancelled === 'string' || typeof cancelled === 'number') {
92
+ this.calls.delete(requestKey(cancelled));
93
+ this.listRequests.delete(requestKey(cancelled));
94
+ }
95
+ this.onmessage?.(message, extra);
96
+ return;
97
+ }
98
+ if (request.method === 'tools/list' && this.visibleTools && request.id !== undefined && request.id !== null) {
99
+ this.listRequests.add(requestKey(request.id));
100
+ this.onmessage?.(message, extra);
101
+ return;
102
+ }
103
+ if (request.method !== 'tools/call' || request.id === undefined || request.id === null || !isObject(request.params)) {
104
+ this.onmessage?.(message, extra);
105
+ return;
106
+ }
107
+ const name = request.params.name;
108
+ const alias = typeof name === 'string' ? resolveAlias(this.registry, name) : null;
109
+ if (!alias) {
110
+ this.onmessage?.(message, extra);
111
+ return;
112
+ }
113
+ const info = {
114
+ calledName: alias.name,
115
+ target: alias.target,
116
+ legacyConfirm: alias.legacyConfirm === true,
117
+ alias,
118
+ };
119
+ this.calls.set(requestKey(request.id), info);
120
+ let callerArgs = request.params.arguments;
121
+ const legacy = this.legacyValidator ? this.legacyValidator(alias.name, callerArgs) : null;
122
+ if (legacy && !legacy.ok) {
123
+ this.respondWithError(request.id, legacy.message);
124
+ return;
125
+ }
126
+ if (legacy?.ok)
127
+ callerArgs = legacy.args;
128
+ // alias.mode pins a param-driven target to the old tool's mode.
129
+ const validated = validateMode(this.registry, alias.target, translateArgs(alias, callerArgs), alias.mode);
130
+ if (!validated.ok) {
131
+ // A plain error before any request, same as the hosted endpoint; the
132
+ // rename note is still appended by send() so the caller learns the new
133
+ // name either way.
134
+ this.respondWithError(request.id, validated.error);
135
+ return;
136
+ }
137
+ const rewritten = {
138
+ ...message,
139
+ params: { ...request.params, name: alias.target, arguments: validated.args },
140
+ };
141
+ this.onmessage?.(rewritten, extra);
142
+ }
143
+ /** Answers an alias call directly with a plain isError result; send() appends the rename note. */
144
+ respondWithError(id, text) {
145
+ void this.send({
146
+ jsonrpc: '2.0',
147
+ id,
148
+ result: { content: [{ type: 'text', text }], isError: true },
149
+ }).catch((error) => this.onerror?.(error instanceof Error ? error : new Error(String(error))));
150
+ }
151
+ filterToolsList(message) {
152
+ const response = message;
153
+ if (!this.visibleTools || response.method !== undefined || response.id === undefined || response.id === null)
154
+ return message;
155
+ const key = requestKey(response.id);
156
+ if (!this.listRequests.has(key))
157
+ return message;
158
+ this.listRequests.delete(key);
159
+ if (!isObject(response.result) || !Array.isArray(response.result.tools))
160
+ return message;
161
+ const visible = this.visibleTools;
162
+ const filtered = {
163
+ ...message,
164
+ result: {
165
+ ...response.result,
166
+ tools: response.result.tools.filter((tool) => typeof tool?.name === 'string' && visible.has(tool.name)),
167
+ },
168
+ };
169
+ return filtered;
170
+ }
171
+ decorateResponse(message) {
172
+ const response = message;
173
+ if (response.method !== undefined || response.id === undefined || response.id === null)
174
+ return message;
175
+ const key = requestKey(response.id);
176
+ const info = this.calls.get(key);
177
+ if (!info)
178
+ return message;
179
+ this.calls.delete(key);
180
+ if (!isObject(response.result) || !Array.isArray(response.result.content))
181
+ return message;
182
+ const note = deprecationNote(info.alias);
183
+ const decorated = {
184
+ ...message,
185
+ result: {
186
+ ...response.result,
187
+ content: [...response.result.content, { type: 'text', text: note.text }],
188
+ _meta: { ...(isObject(response.result._meta) ? response.result._meta : {}), [DEPRECATION_META_KEY]: note.meta },
189
+ },
190
+ };
191
+ return decorated;
192
+ }
193
+ }
@@ -1,3 +1,14 @@
1
+ export declare function setMcpClientNameResolver(resolver: () => string | undefined): void;
2
+ /**
3
+ * HTTP header values must be printable Latin-1 with no control characters;
4
+ * Node's fetch throws (not returns an error, THROWS) on a header value
5
+ * containing anything else, so an MCP client reporting a non-ASCII name
6
+ * (e.g. "通义灵码") or one with an en/em dash would otherwise break every
7
+ * single tool call. Strips anything outside printable ASCII rather than
8
+ * encoding it, since this is just a display label, not data that needs to
9
+ * round-trip losslessly.
10
+ */
11
+ export declare function sanitizeClientHeaderValue(value: string): string;
1
12
  export interface Account {
2
13
  id: string;
3
14
  platform: string;
@@ -320,6 +331,67 @@ export type AskSupportPayload = {
320
331
  confirm_escalation?: boolean;
321
332
  };
322
333
  export type ApiKeyScope = 'accounts:read' | 'accounts:write' | 'posts:read' | 'posts:write' | 'media:write' | 'analytics:read' | 'billing:read' | 'billing:write';
334
+ export type WorkspaceMemberRole = 'publisher' | 'editor' | 'viewer';
335
+ export type WorkspaceMemberSummary = {
336
+ id: string;
337
+ user_id: string | null;
338
+ email: string | null;
339
+ name: string | null;
340
+ role: string | null;
341
+ status: string | null;
342
+ /** [] = every brand; missing = not visible to a brand-limited caller. */
343
+ brand_ids?: string[];
344
+ joined_at: string | null;
345
+ created_at: string | null;
346
+ };
347
+ export type WorkspaceInviteSummary = {
348
+ id: string;
349
+ email: string | null;
350
+ role: string | null;
351
+ status: string | null;
352
+ brand_ids?: string[];
353
+ expires_at: string | null;
354
+ created_at: string | null;
355
+ };
356
+ export type WorkspaceMembersResponse = {
357
+ workspace_id: string;
358
+ members: WorkspaceMemberSummary[];
359
+ invites: WorkspaceInviteSummary[];
360
+ usage: {
361
+ active_members: number;
362
+ pending_invites: number;
363
+ seats_used: number;
364
+ };
365
+ };
366
+ export type WorkspaceMemberPreview = {
367
+ action: string;
368
+ summary: string;
369
+ email: string | null;
370
+ role: string | null;
371
+ previous_role: string | null;
372
+ brand_ids: string[] | null;
373
+ previous_brand_ids: string[] | null;
374
+ sends_email: boolean;
375
+ counts_toward_seats: boolean;
376
+ seats_used: number | null;
377
+ seat_limit: number | null;
378
+ };
379
+ export type WorkspaceMemberChangeResponse = {
380
+ requires_confirmation?: boolean;
381
+ preview?: WorkspaceMemberPreview;
382
+ preview_id?: string;
383
+ preview_expires_at?: string;
384
+ status?: string;
385
+ message?: string;
386
+ invite?: WorkspaceInviteSummary;
387
+ member?: WorkspaceMemberSummary;
388
+ email_sent?: boolean;
389
+ owner_notified?: boolean;
390
+ };
391
+ export type WorkspaceMemberConfirmFields = {
392
+ confirm?: boolean;
393
+ preview_id?: string;
394
+ };
323
395
  export type CreateApiKeyPayload = {
324
396
  name?: string;
325
397
  scopes?: ApiKeyScope[];
@@ -988,6 +1060,13 @@ export declare class PosterlyClient {
988
1060
  constructor(apiKey?: string, baseUrl?: string);
989
1061
  hasApiKey(): boolean;
990
1062
  getBaseUrl(): string;
1063
+ /**
1064
+ * Runs `fn` with a fresh per-call tool name/id scoped via AsyncLocalStorage,
1065
+ * so any request() calls made inside it (directly or nested) are tagged
1066
+ * with X-Posterly-Tool / X-Posterly-Tool-Call. Called once per tool
1067
+ * invocation from index.ts's server.tool() handlers.
1068
+ */
1069
+ runToolCall<T>(toolName: string, fn: () => Promise<T>): Promise<T>;
991
1070
  probeWhoami(timeoutMs?: number): Promise<{
992
1071
  ok: boolean;
993
1072
  status?: number;
@@ -1005,6 +1084,24 @@ export declare class PosterlyClient {
1005
1084
  deleteApiKey(keyId: string, data: {
1006
1085
  confirm: true;
1007
1086
  }): Promise<DeleteApiKeyResponse>;
1087
+ listWorkspaceMembers(workspaceId: string): Promise<WorkspaceMembersResponse>;
1088
+ inviteWorkspaceMember(workspaceId: string, data: {
1089
+ email: string;
1090
+ role: WorkspaceMemberRole;
1091
+ brand_ids?: string[];
1092
+ resend?: boolean;
1093
+ } & WorkspaceMemberConfirmFields): Promise<WorkspaceMemberChangeResponse>;
1094
+ updateWorkspaceMember(workspaceId: string, target: {
1095
+ member_id?: string;
1096
+ invite_id?: string;
1097
+ }, data: {
1098
+ role?: WorkspaceMemberRole;
1099
+ brand_ids?: string[];
1100
+ } & WorkspaceMemberConfirmFields): Promise<WorkspaceMemberChangeResponse>;
1101
+ removeWorkspaceMember(workspaceId: string, target: {
1102
+ member_id?: string;
1103
+ invite_id?: string;
1104
+ }, data: WorkspaceMemberConfirmFields): Promise<WorkspaceMemberChangeResponse>;
1008
1105
  getSubscription(): Promise<SubscriptionSummaryResponse>;
1009
1106
  getCredits(): Promise<CreditsSummaryResponse>;
1010
1107
  cancelSubscription(data: CancelSubscriptionPayload): Promise<CancelSubscriptionResponse>;
@@ -1,5 +1,41 @@
1
1
  import { readFile } from 'fs/promises';
2
2
  import { resolve } from 'path';
3
+ import { AsyncLocalStorage } from 'node:async_hooks';
4
+ import { randomUUID } from 'node:crypto';
5
+ /**
6
+ * Scopes the current tool name/call id to the async execution of a single
7
+ * tool call, so request() can tag its headers without a mutable "current
8
+ * tool" field on the shared PosterlyClient singleton (index.ts registers one
9
+ * client instance for every tool). AsyncLocalStorage keeps overlapping calls
10
+ * from clobbering each other's context if the stdio transport ever
11
+ * dispatches tool calls concurrently.
12
+ */
13
+ const toolCallStorage = new AsyncLocalStorage();
14
+ /**
15
+ * Resolves the connected MCP client's name (e.g. "claude-ai", "chatgpt"), set
16
+ * once by index.ts after the server is constructed. A resolver function
17
+ * rather than a static value because the SDK only learns the client's
18
+ * identity once the initialize handshake completes, which happens after this
19
+ * module is first loaded; reading through the resolver on every request
20
+ * always gets the current value instead of whatever was available at import
21
+ * time (undefined).
22
+ */
23
+ let clientNameResolver = null;
24
+ export function setMcpClientNameResolver(resolver) {
25
+ clientNameResolver = resolver;
26
+ }
27
+ /**
28
+ * HTTP header values must be printable Latin-1 with no control characters;
29
+ * Node's fetch throws (not returns an error, THROWS) on a header value
30
+ * containing anything else, so an MCP client reporting a non-ASCII name
31
+ * (e.g. "通义灵码") or one with an en/em dash would otherwise break every
32
+ * single tool call. Strips anything outside printable ASCII rather than
33
+ * encoding it, since this is just a display label, not data that needs to
34
+ * round-trip losslessly.
35
+ */
36
+ export function sanitizeClientHeaderValue(value) {
37
+ return value.replace(/[^\x20-\x7e]/g, '').trim().slice(0, 200);
38
+ }
3
39
  export class PosterlyClient {
4
40
  baseUrl;
5
41
  apiKey;
@@ -16,6 +52,15 @@ export class PosterlyClient {
16
52
  getBaseUrl() {
17
53
  return this.baseUrl;
18
54
  }
55
+ /**
56
+ * Runs `fn` with a fresh per-call tool name/id scoped via AsyncLocalStorage,
57
+ * so any request() calls made inside it (directly or nested) are tagged
58
+ * with X-Posterly-Tool / X-Posterly-Tool-Call. Called once per tool
59
+ * invocation from index.ts's server.tool() handlers.
60
+ */
61
+ async runToolCall(toolName, fn) {
62
+ return toolCallStorage.run({ toolName, toolCallId: randomUUID() }, fn);
63
+ }
19
64
  async probeWhoami(timeoutMs = 2500) {
20
65
  const startedAt = Date.now();
21
66
  if (!this.apiKey) {
@@ -78,6 +123,16 @@ export class PosterlyClient {
78
123
  Authorization: `Bearer ${this.apiKey}`,
79
124
  'X-Posterly-Source': 'mcp_stdio',
80
125
  };
126
+ const toolCall = toolCallStorage.getStore();
127
+ if (toolCall) {
128
+ headers['X-Posterly-Tool'] = toolCall.toolName;
129
+ headers['X-Posterly-Tool-Call'] = toolCall.toolCallId;
130
+ }
131
+ const clientName = clientNameResolver?.();
132
+ const clientLabel = clientName ? sanitizeClientHeaderValue(clientName) : '';
133
+ if (clientLabel) {
134
+ headers['X-Posterly-Client'] = clientLabel;
135
+ }
81
136
  const init = { method, headers };
82
137
  if (body instanceof FormData) {
83
138
  init.body = body;
@@ -123,6 +178,32 @@ export class PosterlyClient {
123
178
  async deleteApiKey(keyId, data) {
124
179
  return this.request('DELETE', `/api-keys/${encodeURIComponent(keyId)}`, data);
125
180
  }
181
+ async listWorkspaceMembers(workspaceId) {
182
+ return this.request('GET', `/workspaces/${encodeURIComponent(workspaceId)}/members`);
183
+ }
184
+ async inviteWorkspaceMember(workspaceId, data) {
185
+ return this.request('POST', `/workspaces/${encodeURIComponent(workspaceId)}/members`, data);
186
+ }
187
+ async updateWorkspaceMember(workspaceId, target, data) {
188
+ const base = `/workspaces/${encodeURIComponent(workspaceId)}`;
189
+ const path = target.member_id
190
+ ? `${base}/members/${encodeURIComponent(target.member_id)}`
191
+ : `${base}/invites/${encodeURIComponent(target.invite_id || '')}`;
192
+ return this.request('PATCH', path, data);
193
+ }
194
+ async removeWorkspaceMember(workspaceId, target, data) {
195
+ const base = `/workspaces/${encodeURIComponent(workspaceId)}`;
196
+ const path = target.member_id
197
+ ? `${base}/members/${encodeURIComponent(target.member_id)}`
198
+ : `${base}/invites/${encodeURIComponent(target.invite_id || '')}`;
199
+ const params = new URLSearchParams();
200
+ if (data.confirm === true)
201
+ params.set('confirm', 'true');
202
+ if (data.preview_id)
203
+ params.set('preview_id', data.preview_id);
204
+ const qs = params.toString();
205
+ return this.request('DELETE', `${path}${qs ? `?${qs}` : ''}`);
206
+ }
126
207
  async getSubscription() {
127
208
  return this.request('GET', '/subscription');
128
209
  }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Preview-then-confirm tokens for MCP write actions, shared by both surfaces.
3
+ * Depends on node:crypto only.
4
+ *
5
+ * Format: pv1.<exp>.<nonce>.<mac>
6
+ * exp unix seconds when the preview stops being confirmable
7
+ * nonce random, base64url; also the one-time-use key for nonce actions
8
+ * mac base64url HMAC-SHA256(key,
9
+ * "posterly-mcp-preview-v1|" + exp + "|" + nonce + "|" + userId
10
+ * + "|" + tool + "|" + action + "|" + canonicalJson(args))
11
+ *
12
+ * The token binds the preview to one user, one tool, one action, and the
13
+ * exact (normalised) arguments, so a confirm call with different args, from a
14
+ * different user, or after expiry is refused. It is UX protection that a
15
+ * human saw what will change, not authorization: REST scopes and ownership
16
+ * checks still decide what is allowed.
17
+ *
18
+ * Callers own the key (hosted: derived from a server secret; stdio: random
19
+ * per process) and the one-time-use store (hosted: Redis; stdio: memory).
20
+ */
21
+ export declare const PREVIEW_TOKEN_VERSION = "pv1";
22
+ export interface PreviewBinding {
23
+ userId: string;
24
+ tool: string;
25
+ action: string;
26
+ /** Arguments AFTER validation/normalisation (defaults applied, numbers coerced). */
27
+ args: unknown;
28
+ }
29
+ export type VerifyPreviewResult = {
30
+ ok: true;
31
+ exp: number;
32
+ nonce: string;
33
+ remainingSeconds: number;
34
+ } | {
35
+ ok: false;
36
+ reason: 'malformed' | 'expired' | 'mismatch';
37
+ };
38
+ /**
39
+ * Canonical JSON of a call's arguments: `confirm` and `preview_id` removed
40
+ * (top level), undefined dropped, keys sorted recursively, arrays kept in
41
+ * order, no whitespace. Same rules as canonicalArgs in tool-registry.ts.
42
+ */
43
+ export declare function canonicalJson(args: unknown): string;
44
+ /** Derives the MAC key from a server secret with a domain prefix, so the raw secret is never the key. */
45
+ export declare function derivePreviewKey(secret: string | Buffer, domain?: string): Buffer;
46
+ export declare function createPreviewToken(key: Buffer, binding: PreviewBinding, options: {
47
+ ttlSeconds: number;
48
+ nowMs?: number;
49
+ nonce?: string;
50
+ }): string;
51
+ export declare function parsePreviewToken(token: unknown): {
52
+ exp: number;
53
+ nonce: string;
54
+ mac: string;
55
+ } | null;
56
+ export declare function verifyPreviewToken(key: Buffer, token: unknown, binding: PreviewBinding, options?: {
57
+ nowMs?: number;
58
+ }): VerifyPreviewResult;
59
+ /** Stable fingerprint of a token for one-time-use bookkeeping (never store the raw token). */
60
+ export declare function previewTokenFingerprint(token: string): string;
61
+ /** Plain-language refusal for each verification failure. */
62
+ export declare function previewFailureMessage(reason: 'malformed' | 'expired' | 'mismatch' | 'used' | 'missing' | 'unavailable'): string;