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,394 @@
1
+ /**
2
+ * Pure, dependency-free helpers over the MCP tool registry
3
+ * (mcp-server/tool-registry.json). Shared by BOTH surfaces:
4
+ * - the npm stdio package (mcp-server/src/index.ts, alias-transport.ts)
5
+ * - the hosted endpoint (lib/mcp/tool-registry.ts re-exports these with the
6
+ * registry bound)
7
+ *
8
+ * This module never imports the JSON itself: each surface loads the registry
9
+ * its own way (createRequire on stdio, a static JSON import on hosted) and
10
+ * passes it in. Keep it free of Node, Next, and zod imports so it compiles
11
+ * unchanged under both the package tsconfig and the app tsconfig.
12
+ */
13
+ /** The alias entry for `name`, or null when `name` is not an alias. */
14
+ export function resolveAlias(registry, name) {
15
+ if (!Object.prototype.hasOwnProperty.call(registry.aliases, name))
16
+ return null;
17
+ return { name, ...registry.aliases[name] };
18
+ }
19
+ function isPlainObject(value) {
20
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
21
+ }
22
+ /**
23
+ * Translates an alias call's arguments into the target tool's arguments.
24
+ * Always returns a fresh object (never mutates the caller's args): renames
25
+ * first, then injected params, which win over anything the caller passed.
26
+ */
27
+ export function translateArgs(alias, args) {
28
+ const source = isPlainObject(args) ? args : {};
29
+ const out = {};
30
+ for (const [key, value] of Object.entries(source)) {
31
+ const renamed = alias.rename && Object.prototype.hasOwnProperty.call(alias.rename, key) ? alias.rename[key] : key;
32
+ out[renamed] = value;
33
+ }
34
+ for (const [key, value] of Object.entries(alias.inject ?? {})) {
35
+ out[key] = value;
36
+ }
37
+ return out;
38
+ }
39
+ const MODE_COMMON_PARAMS = new Set(['confirm', 'preview_id']);
40
+ function modeEntries(tool) {
41
+ return tool.views ?? tool.actions ?? null;
42
+ }
43
+ function normalizeParam(name, value, spec, where) {
44
+ switch (spec.type) {
45
+ case 'integer':
46
+ case 'number': {
47
+ let n = value;
48
+ if (typeof value === 'string' && value.trim() !== '' && /^-?\d+(\.\d+)?$/.test(value.trim()))
49
+ n = Number(value.trim());
50
+ if (typeof n !== 'number' || !Number.isFinite(n))
51
+ return { ok: false, error: `${name} must be a number${where}.` };
52
+ if (spec.type === 'integer' && !Number.isInteger(n))
53
+ return { ok: false, error: `${name} must be a whole number${where}.` };
54
+ if (spec.min != null && n < spec.min)
55
+ return { ok: false, error: `${name} must be at least ${spec.min}${where}.` };
56
+ if (spec.max != null && n > spec.max)
57
+ return { ok: false, error: `${name} must be at most ${spec.max}${where}.` };
58
+ if (spec.enum && !spec.enum.includes(n))
59
+ return { ok: false, error: `${name} must be one of ${spec.enum.join(', ')}${where}.` };
60
+ return { ok: true, value: n };
61
+ }
62
+ case 'string': {
63
+ if (typeof value === 'number' && Number.isFinite(value))
64
+ value = String(value);
65
+ if (typeof value !== 'string')
66
+ return { ok: false, error: `${name} must be a string${where}.` };
67
+ if (spec.enum && !spec.enum.includes(value))
68
+ return { ok: false, error: `${name} must be one of ${spec.enum.join(', ')}${where}.` };
69
+ return { ok: true, value };
70
+ }
71
+ case 'boolean':
72
+ if (typeof value !== 'boolean')
73
+ return { ok: false, error: `${name} must be true or false${where}.` };
74
+ return { ok: true, value };
75
+ case 'array':
76
+ if (!Array.isArray(value))
77
+ return { ok: false, error: `${name} must be a list${where}.` };
78
+ return { ok: true, value };
79
+ case 'object':
80
+ if (!isPlainObject(value))
81
+ return { ok: false, error: `${name} must be an object${where}.` };
82
+ return { ok: true, value };
83
+ default:
84
+ return { ok: true, value };
85
+ }
86
+ }
87
+ function isPresent(value) {
88
+ return value !== undefined && value !== null && value !== '';
89
+ }
90
+ /**
91
+ * The mode a param-driven tool (selector "params") runs in for these args:
92
+ * the first mode whose `when` params are present, otherwise the tool's
93
+ * default mode (the one without `when`). Undefined for other tools.
94
+ */
95
+ export function detectParamsMode(tool, args) {
96
+ if (tool.selector !== 'params')
97
+ return undefined;
98
+ const entries = modeEntries(tool);
99
+ if (!entries)
100
+ return undefined;
101
+ const source = isPlainObject(args) ? args : {};
102
+ for (const [mode, entry] of Object.entries(entries)) {
103
+ if (entry.when?.some((key) => isPresent(source[key])))
104
+ return mode;
105
+ }
106
+ if (tool.defaultMode !== undefined)
107
+ return tool.defaultMode;
108
+ return Object.entries(entries).find(([, entry]) => !entry.when)?.[0];
109
+ }
110
+ /**
111
+ * Validates and normalises a call's arguments against the registry's per-mode
112
+ * rules, BEFORE any request is made. It is the normaliser on both surfaces
113
+ * (the hosted endpoint has no zod), so a preview call and its confirm call
114
+ * hash identically: numeric strings become numbers, defaults are applied, and
115
+ * params outside the selected mode are rejected with a plain error.
116
+ *
117
+ * `explicitMode` forces a mode for param-driven tools (selector "params"), for
118
+ * example an alias of the old get_comment tool always runs in the comment
119
+ * mode. Without it, a param-driven tool's mode is detected from which params
120
+ * are present (see detectParamsMode).
121
+ *
122
+ * Tools without views/actions, and modes without a `params` spec, pass
123
+ * through unchanged (a fresh copy).
124
+ */
125
+ export function validateMode(registry, toolName, args, explicitMode) {
126
+ const tool = registry.tools[toolName];
127
+ const source = isPlainObject(args) ? { ...args } : {};
128
+ if (!tool)
129
+ return { ok: false, error: `Unknown tool: ${toolName}` };
130
+ const entries = modeEntries(tool);
131
+ if (!entries || !tool.selector)
132
+ return { ok: true, mode: null, args: source };
133
+ const selector = tool.selector;
134
+ let mode = explicitMode;
135
+ if (selector === 'params' && mode === undefined) {
136
+ mode = detectParamsMode(tool, source);
137
+ }
138
+ else if (selector !== 'params') {
139
+ const raw = source[selector];
140
+ if (raw === undefined || raw === null || raw === '') {
141
+ mode = tool.defaultMode;
142
+ if (mode === undefined) {
143
+ return { ok: false, error: `${selector} is required. Use one of: ${Object.keys(entries).join(', ')}.` };
144
+ }
145
+ source[selector] = mode;
146
+ }
147
+ else if (typeof raw !== 'string' || !Object.prototype.hasOwnProperty.call(entries, raw)) {
148
+ return { ok: false, error: `${selector} must be one of: ${Object.keys(entries).join(', ')}.` };
149
+ }
150
+ else {
151
+ mode = raw;
152
+ }
153
+ }
154
+ if (mode === undefined)
155
+ return { ok: true, mode: null, args: source };
156
+ const entry = entries[mode];
157
+ if (!entry)
158
+ return { ok: false, error: `Unknown ${selector === 'params' ? 'mode' : selector} ${mode} for ${toolName}.` };
159
+ if (!entry.params)
160
+ return { ok: true, mode, args: source };
161
+ const where = selector === 'params' ? '' : ` with ${selector}=${mode}`;
162
+ const notAllowedWhere = selector === 'params'
163
+ ? (entry.when?.length ? ` together with ${entry.when.join(' or ')}` : '')
164
+ : where;
165
+ for (const key of Object.keys(source)) {
166
+ if (key === selector || MODE_COMMON_PARAMS.has(key))
167
+ continue;
168
+ if (!Object.prototype.hasOwnProperty.call(entry.params, key)) {
169
+ // Many clients send every optional field, unset ones as null or "".
170
+ // Those carry no value, so they are dropped (never rejected), which
171
+ // also keeps a preview call and its confirm call hashing identically.
172
+ if (!isPresent(source[key])) {
173
+ delete source[key];
174
+ continue;
175
+ }
176
+ return { ok: false, error: `${key} is not allowed${notAllowedWhere}.` };
177
+ }
178
+ }
179
+ for (const [name, spec] of Object.entries(entry.params)) {
180
+ // The old hosted tools skipped falsy values (`if (input?.limit)`), so an
181
+ // empty string for a non-string param still means "not given".
182
+ if (source[name] === '' && spec.type && spec.type !== 'string')
183
+ delete source[name];
184
+ const value = source[name];
185
+ if (value === null && spec.nullable)
186
+ continue;
187
+ if (value === undefined || value === null) {
188
+ if (spec.required)
189
+ return { ok: false, error: `${name} is required${where}.` };
190
+ if (spec.default !== undefined)
191
+ source[name] = spec.default;
192
+ else
193
+ delete source[name];
194
+ continue;
195
+ }
196
+ const normalized = normalizeParam(name, value, spec, where);
197
+ if (!normalized.ok)
198
+ return normalized;
199
+ source[name] = normalized.value;
200
+ }
201
+ return { ok: true, mode, args: source };
202
+ }
203
+ function sortKeysDeep(value) {
204
+ if (Array.isArray(value))
205
+ return value.map((item) => (item === undefined ? null : sortKeysDeep(item)));
206
+ if (isPlainObject(value)) {
207
+ const out = {};
208
+ for (const key of Object.keys(value).sort()) {
209
+ const item = value[key];
210
+ if (item === undefined)
211
+ continue;
212
+ out[key] = sortKeysDeep(item);
213
+ }
214
+ return out;
215
+ }
216
+ return value;
217
+ }
218
+ /**
219
+ * The canonical form of a call's arguments for hashing: `confirm` and
220
+ * `preview_id` removed, undefined dropped, object keys sorted recursively,
221
+ * arrays kept in order. Pass args AFTER validateMode so defaults and number
222
+ * coercion are already applied.
223
+ */
224
+ export function canonicalArgs(args) {
225
+ const source = isPlainObject(args) ? args : {};
226
+ const stripped = {};
227
+ for (const [key, value] of Object.entries(source)) {
228
+ if (key === 'confirm' || key === 'preview_id')
229
+ continue;
230
+ stripped[key] = value;
231
+ }
232
+ return sortKeysDeep(stripped);
233
+ }
234
+ function injectedMode(alias) {
235
+ const inject = alias.inject ?? {};
236
+ for (const key of ['action', 'view', 'type']) {
237
+ const value = inject[key];
238
+ if (typeof value === 'string')
239
+ return { key, value };
240
+ }
241
+ return null;
242
+ }
243
+ /**
244
+ * The note appended (as a final text content item, never touching content[0])
245
+ * to every alias call's result, plus the matching _meta payload. `action` in
246
+ * the payload is the injected view/action/type value (null when none).
247
+ */
248
+ export function deprecationNote(alias) {
249
+ const mode = injectedMode(alias);
250
+ const target = mode ? `${alias.target} with ${mode.key}=${mode.value}` : alias.target;
251
+ return {
252
+ text: `Renamed: ${alias.name} is now ${target}. The old name may stop working after ${alias.sunset}.`,
253
+ meta: { old: alias.name, new: alias.target, action: mode ? mode.value : null, sunset: alias.sunset },
254
+ };
255
+ }
256
+ export const DEPRECATION_META_KEY = 'posterly/deprecation';
257
+ /**
258
+ * Parses a toolset selection such as "core,inbox" or "all". Empty, missing,
259
+ * or all-unknown input means every set (nobody loses tools by mistake).
260
+ * With a surface, sets not available on it (for example signup, stdio only,
261
+ * on the hosted endpoint) are treated as unknown.
262
+ */
263
+ export function parseToolsets(registry, value, surface) {
264
+ const available = Object.entries(registry.toolsets)
265
+ .filter(([, set]) => !surface || !set.surfaces || set.surfaces.includes(surface))
266
+ .map(([name]) => name);
267
+ const requested = String(value ?? '')
268
+ .split(',')
269
+ .map((part) => part.trim().toLowerCase())
270
+ .filter(Boolean);
271
+ if (requested.length === 0 || requested.includes('all')) {
272
+ return { enabled: available, unknown: requested.filter((name) => name !== 'all' && !available.includes(name)), all: true };
273
+ }
274
+ const enabled = available.filter((name) => requested.includes(name));
275
+ const unknown = requested.filter((name) => !available.includes(name));
276
+ if (enabled.length === 0)
277
+ return { enabled: available, unknown, all: true };
278
+ return { enabled, unknown, all: enabled.length === available.length };
279
+ }
280
+ /**
281
+ * Names of the canonical tools visible on `surface` for the enabled sets.
282
+ * Aliases are never listed. Sets marked alwaysOn (signup on stdio) are always
283
+ * visible. Surfaces keep their own registration order by filtering with this.
284
+ */
285
+ export function visibleTools(registry, sets, surface) {
286
+ const enabled = new Set(sets);
287
+ const visible = new Set();
288
+ for (const [name, tool] of Object.entries(registry.tools)) {
289
+ const surfaces = tool.surfaces ?? ['hosted', 'stdio'];
290
+ if (!surfaces.includes(surface))
291
+ continue;
292
+ const set = registry.toolsets[tool.set];
293
+ if (enabled.has(tool.set) || set?.alwaysOn)
294
+ visible.add(name);
295
+ }
296
+ return visible;
297
+ }
298
+ /**
299
+ * Annotation hints derived from the registry: a multi-action tool is
300
+ * destructive if any action is, idempotent only if all are, openWorld if any
301
+ * is; multi-view reads are readOnly + idempotent, openWorld if any view is.
302
+ */
303
+ export function deriveAnnotations(tool) {
304
+ if (tool.views && !tool.actions) {
305
+ const views = Object.values(tool.views);
306
+ return {
307
+ readOnlyHint: true,
308
+ destructiveHint: false,
309
+ idempotentHint: true,
310
+ openWorldHint: views.some((view) => view.openWorld === true),
311
+ };
312
+ }
313
+ if (tool.actions) {
314
+ const actions = Object.values(tool.actions);
315
+ return {
316
+ readOnlyHint: false,
317
+ destructiveHint: actions.some((action) => action.destructive === true),
318
+ idempotentHint: actions.length > 0 && actions.every((action) => action.idempotent === true),
319
+ openWorldHint: actions.some((action) => action.openWorld === true),
320
+ };
321
+ }
322
+ const readOnly = tool.readOnly === true;
323
+ return {
324
+ readOnlyHint: readOnly,
325
+ destructiveHint: !readOnly && tool.destructive === true,
326
+ idempotentHint: readOnly || tool.idempotent === true,
327
+ openWorldHint: tool.openWorld === true,
328
+ };
329
+ }
330
+ /** Actions (sub tools) a tool counts for: its views/actions, otherwise 1. */
331
+ export function countActions(tool) {
332
+ const entries = modeEntries(tool);
333
+ return entries ? Math.max(1, Object.keys(entries).length) : 1;
334
+ }
335
+ /**
336
+ * Every canonical tool on `surface`, grouped by tool set in registry order,
337
+ * each with its views/actions and their labels. This is what every tool list
338
+ * in the docs, marketing pages, and server card renders from, so the "sub
339
+ * tools" inside a merged tool are always shown. Aliases are never listed.
340
+ */
341
+ export function listToolsBySet(registry, surface) {
342
+ const groups = [];
343
+ for (const [set, info] of Object.entries(registry.toolsets)) {
344
+ if (info.surfaces && !info.surfaces.includes(surface))
345
+ continue;
346
+ const tools = [];
347
+ for (const [name, tool] of Object.entries(registry.tools)) {
348
+ if (tool.set !== set)
349
+ continue;
350
+ if (!(tool.surfaces ?? ['hosted', 'stdio']).includes(surface))
351
+ continue;
352
+ const entries = modeEntries(tool);
353
+ tools.push({
354
+ name,
355
+ label: tool.label,
356
+ selector: entries && tool.selector && tool.selector !== 'params' ? tool.selector : null,
357
+ actions: entries ? Object.entries(entries).map(([key, entry]) => ({ key, label: entry.label })) : [],
358
+ });
359
+ }
360
+ if (tools.length > 0)
361
+ groups.push({ set, label: info.label, description: info.description, tools });
362
+ }
363
+ return groups;
364
+ }
365
+ /**
366
+ * What a called name resolves to: the canonical tool plus the view/action it
367
+ * runs (for an alias), or the name itself. Used by /admin/mcp to show old
368
+ * names next to the tool they now reach, without any new telemetry column.
369
+ */
370
+ export function resolveCalledTool(registry, calledName) {
371
+ const alias = resolveAlias(registry, calledName);
372
+ if (!alias)
373
+ return { called: calledName, tool: calledName, mode: null, alias: false };
374
+ const injected = injectedMode(alias);
375
+ return { called: calledName, tool: alias.target, mode: injected?.value ?? alias.mode ?? null, alias: true };
376
+ }
377
+ /**
378
+ * The initialize `instructions` text: which tool sets exist, that they are
379
+ * all on by default, how to load fewer, and how preview then confirm works.
380
+ */
381
+ export function toolsetInstructions(registry, surface) {
382
+ const sets = Object.entries(registry.toolsets)
383
+ .filter(([, set]) => !set.surfaces || set.surfaces.includes(surface))
384
+ .map(([name, set]) => `${name} (${set.description.split('. ')[0].replace(/\.$/, '')})`);
385
+ const howTo = surface === 'hosted'
386
+ ? 'To load fewer tools with an API key in the Authorization header, add ?toolsets=core,inbox to the endpoint URL. OAuth connectors always get every set.'
387
+ : 'To load fewer tools, set POSTERLY_TOOLSETS=core,inbox (or pass --toolsets=core,inbox). The signup tools are always on.';
388
+ return [
389
+ `posterly groups its tools into sets: ${sets.join('; ')}.`,
390
+ `Every set is on by default. ${howTo} Sets only change which tools are listed; any tool can still be called by name.`,
391
+ 'Merged tools pick what to do with a view, action, or type param; each tool description names every option.',
392
+ 'Tools that post publicly or delete things use preview then confirm: call once without confirm to get a preview and a preview_id, show it to the user, then call again with confirm: true and that preview_id.',
393
+ ].join(' ');
394
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Per-call context and shared helpers for the npm stdio tools that merge
3
+ * several old tools (MCP tool consolidation, 0.45.0).
4
+ *
5
+ * - validateArgs(): the registry's per-mode rules (mcp-server/tool-registry.json)
6
+ * run before any request, same as the hosted endpoint
7
+ * - previewThenConfirm(): preview then confirm for public or destructive
8
+ * actions; alias calls (ctx.legacyConfirm) keep their old one-step confirm
9
+ * until the alias sunset date
10
+ *
11
+ * The context is built per call in index.ts from the transport's request map,
12
+ * never stored in the arguments or on shared state.
13
+ */
14
+ import { type ToolRegistry } from './tool-registry.js';
15
+ import type { StdioPreviewTokens } from './preview-token.js';
16
+ export interface ToolContext {
17
+ /** The name the client called: an alias or the canonical tool. */
18
+ calledName: string;
19
+ /** True for alias calls: confirm: true alone still runs preview actions until the sunset date. */
20
+ legacyConfirm: boolean;
21
+ registry: ToolRegistry;
22
+ previews: StdioPreviewTokens;
23
+ /** The tool sets listed in this process (POSTERLY_TOOLSETS); every set when all is true. */
24
+ toolsets?: {
25
+ enabled: string[];
26
+ all: boolean;
27
+ };
28
+ }
29
+ /**
30
+ * Validates a merged tool's args against the registry's per-mode rules and
31
+ * returns the normalised args and mode. Throws a plain error before any request.
32
+ */
33
+ export declare function validateArgs(ctx: ToolContext, tool: string, input: unknown): {
34
+ mode: string | null;
35
+ args: Record<string, any>;
36
+ };
37
+ /** A shallow copy of args without the given keys. */
38
+ export declare function omitKeys(args: Record<string, any>, keys: string[]): Record<string, any>;
39
+ export interface PreviewStep {
40
+ tool: string;
41
+ action: string;
42
+ /** Validated (normalised) args: the preview and the confirm call must hash identically. */
43
+ args: Record<string, any>;
44
+ /** Public writes: a preview_id can be confirmed once only. */
45
+ nonce: boolean;
46
+ /** The old tool's refusal when confirm is missing, for alias calls. */
47
+ legacyRefusal: string;
48
+ ctx: ToolContext;
49
+ /** What will change (may make one read request; must not throw). */
50
+ describe: () => Promise<string>;
51
+ run: () => Promise<string>;
52
+ }
53
+ /**
54
+ * Preview then confirm (docs/mcp-tool-design.md section 4). Canonical calls:
55
+ * no confirm returns a preview and a preview_id bound to this tool, action,
56
+ * and exact args; confirm needs that preview_id. Alias calls keep the old
57
+ * contract until the sunset: confirm: true alone runs the action.
58
+ */
59
+ export declare function previewThenConfirm(step: PreviewStep): Promise<string>;
60
+ /** Runs one read for a preview; a failed read (for example a 403 on a narrower key) falls back to echoing the request. */
61
+ export declare function readForPreview<T>(read: () => Promise<T>): Promise<{
62
+ ok: true;
63
+ data: T;
64
+ } | {
65
+ ok: false;
66
+ error: string;
67
+ }>;
68
+ export declare function readFailedNote(error: string): string;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Per-call context and shared helpers for the npm stdio tools that merge
3
+ * several old tools (MCP tool consolidation, 0.45.0).
4
+ *
5
+ * - validateArgs(): the registry's per-mode rules (mcp-server/tool-registry.json)
6
+ * run before any request, same as the hosted endpoint
7
+ * - previewThenConfirm(): preview then confirm for public or destructive
8
+ * actions; alias calls (ctx.legacyConfirm) keep their old one-step confirm
9
+ * until the alias sunset date
10
+ *
11
+ * The context is built per call in index.ts from the transport's request map,
12
+ * never stored in the arguments or on shared state.
13
+ */
14
+ import { canonicalArgs, validateMode } from './tool-registry.js';
15
+ import { previewFailureMessage } from './preview-token-core.js';
16
+ import { code, mdSection, mdTitle } from './format.js';
17
+ /** The local process serves one user; preview tokens are bound to this process's key anyway. */
18
+ const STDIO_PREVIEW_USER = 'stdio-local';
19
+ /**
20
+ * Validates a merged tool's args against the registry's per-mode rules and
21
+ * returns the normalised args and mode. Throws a plain error before any request.
22
+ */
23
+ export function validateArgs(ctx, tool, input) {
24
+ const validated = validateMode(ctx.registry, tool, input);
25
+ if (!validated.ok)
26
+ throw new Error(validated.error);
27
+ return { mode: validated.mode, args: validated.args };
28
+ }
29
+ /** A shallow copy of args without the given keys. */
30
+ export function omitKeys(args, keys) {
31
+ const out = {};
32
+ for (const [key, value] of Object.entries(args || {})) {
33
+ if (!keys.includes(key))
34
+ out[key] = value;
35
+ }
36
+ return out;
37
+ }
38
+ function formatPreview(body, previewId, tool) {
39
+ return [
40
+ mdTitle('Preview only: nothing has changed yet'),
41
+ body,
42
+ mdSection('Preview ID', `${code(previewId)} (expires in 10 minutes)`),
43
+ mdSection('Next step', `Show this preview to the user. Only after they explicitly approve, call ${tool} again with the same fields plus confirm: true and this preview_id.`),
44
+ ].filter(Boolean).join('\n\n');
45
+ }
46
+ /**
47
+ * Preview then confirm (docs/mcp-tool-design.md section 4). Canonical calls:
48
+ * no confirm returns a preview and a preview_id bound to this tool, action,
49
+ * and exact args; confirm needs that preview_id. Alias calls keep the old
50
+ * contract until the sunset: confirm: true alone runs the action.
51
+ */
52
+ export async function previewThenConfirm(step) {
53
+ const { ctx, args } = step;
54
+ if (ctx.legacyConfirm) {
55
+ if (args.confirm !== true)
56
+ throw new Error(step.legacyRefusal);
57
+ return step.run();
58
+ }
59
+ const binding = { userId: STDIO_PREVIEW_USER, tool: step.tool, action: step.action, args: canonicalArgs(args) };
60
+ if (args.confirm !== true) {
61
+ const previewId = ctx.previews.create(binding);
62
+ return formatPreview(await step.describe(), previewId, step.tool);
63
+ }
64
+ const token = args.preview_id;
65
+ if (typeof token !== 'string' || !token)
66
+ throw new Error(previewFailureMessage('missing'));
67
+ const verified = ctx.previews.verify(token, binding);
68
+ if (!verified.ok)
69
+ throw new Error(previewFailureMessage(verified.reason));
70
+ if (step.nonce && !ctx.previews.claimOnce(token, verified.exp))
71
+ throw new Error(previewFailureMessage('used'));
72
+ return step.run();
73
+ }
74
+ /** Runs one read for a preview; a failed read (for example a 403 on a narrower key) falls back to echoing the request. */
75
+ export async function readForPreview(read) {
76
+ try {
77
+ return { ok: true, data: await read() };
78
+ }
79
+ catch (error) {
80
+ return { ok: false, error: error?.message || String(error) };
81
+ }
82
+ }
83
+ export function readFailedNote(error) {
84
+ return `_posterly could not read the current item (${error}), so the preview shows the request as given._`;
85
+ }
@@ -1 +1 @@
1
- export declare const POSTERLY_MCP_VERSION = "0.43.8";
1
+ export declare const POSTERLY_MCP_VERSION = "0.45.0";
@@ -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.43.8';
8
+ export const POSTERLY_MCP_VERSION = '0.45.0';
@@ -5,7 +5,7 @@ export const auditGoogleBusinessProfileTool = {
5
5
  inputSchema: z.object({
6
6
  workspace_id: z.string().optional().describe('Filter to a workspace ID from whoami.'),
7
7
  account_id: z.number().int().positive().optional().describe('Google Business account ID from list_accounts.'),
8
- location_id: z.string().optional().describe('Google Business numeric location id (the location_id/platform_user_id from list_accounts, e.g. "197940849675145390"). NOT the ChIJ... Place ID returned by get_google_business_review_link. Prefer account_id.'),
8
+ location_id: z.string().optional().describe('Google Business numeric location id (the location_id/platform_user_id from list_accounts, e.g. "197940849675145390"). NOT the ChIJ... Place ID from list_google_business_reviews view=review_link. Prefer account_id.'),
9
9
  }),
10
10
  async execute(client, input) {
11
11
  const result = await client.auditGoogleBusinessProfile(input);
@@ -20,13 +20,13 @@ export declare const connectAccountTool: {
20
20
  workspace_id: z.ZodOptional<z.ZodString>;
21
21
  connect_session_id: z.ZodOptional<z.ZodString>;
22
22
  }, "strip", z.ZodTypeAny, {
23
- platform: "telegram" | "bluesky" | "discord" | "devto" | "hashnode" | "wordpress" | "lemmy";
23
+ platform: "telegram" | "bluesky" | "discord" | "wordpress" | "devto" | "hashnode" | "lemmy";
24
24
  username?: string | undefined;
25
25
  workspace_id?: string | undefined;
26
26
  connect_session_id?: string | undefined;
27
- handle?: string | undefined;
28
27
  bot_token?: string | undefined;
29
28
  chat_id?: string | undefined;
29
+ handle?: string | undefined;
30
30
  app_password?: string | undefined;
31
31
  webhook_url?: string | undefined;
32
32
  site_url?: string | undefined;
@@ -35,13 +35,13 @@ export declare const connectAccountTool: {
35
35
  instance?: string | undefined;
36
36
  password?: string | undefined;
37
37
  }, {
38
- platform: "telegram" | "bluesky" | "discord" | "devto" | "hashnode" | "wordpress" | "lemmy";
38
+ platform: "telegram" | "bluesky" | "discord" | "wordpress" | "devto" | "hashnode" | "lemmy";
39
39
  username?: string | undefined;
40
40
  workspace_id?: string | undefined;
41
41
  connect_session_id?: string | undefined;
42
- handle?: string | undefined;
43
42
  bot_token?: string | undefined;
44
43
  chat_id?: string | undefined;
44
+ handle?: string | undefined;
45
45
  app_password?: string | undefined;
46
46
  webhook_url?: string | undefined;
47
47
  site_url?: string | undefined;
@@ -26,7 +26,7 @@ const PLATFORM_CREDENTIAL_FIELDS = {
26
26
  };
27
27
  export const connectAccountTool = {
28
28
  name: 'connect_account',
29
- description: 'Connect a credential-based social account (telegram, bluesky, discord, wordpress, devto, hashnode, lemmy) directly, without a browser session. Call get_connect_link first to discover the exact credential fields the platform needs. Prefer scoped secrets: app passwords (Bluesky, WordPress), bot tokens (Telegram), webhook URLs (Discord), and API tokens (Dev.to, Hashnode) over primary passwords. Warn the user that any credential they share passes through this conversation. For OAuth platforms (Instagram, X, LinkedIn, ...) use create_connect_session instead.',
29
+ description: 'Connect a credential-based social account (telegram, bluesky, discord, wordpress, devto, hashnode, lemmy) directly, without a browser session. Call list_platforms with view=connect_link first to discover the exact credential fields the platform needs. Prefer scoped secrets: app passwords (Bluesky, WordPress), bot tokens (Telegram), webhook URLs (Discord), and API tokens (Dev.to, Hashnode) over primary passwords. Warn the user that any credential they share passes through this conversation. For OAuth platforms (Instagram, X, LinkedIn, ...) use create_connect_session instead.',
30
30
  inputSchema: z.object({
31
31
  platform: z
32
32
  .enum(CREDENTIAL_PLATFORMS)
@@ -56,7 +56,7 @@ export const connectAccountTool = {
56
56
  }
57
57
  const missing = allowedFields.filter((field) => !(field in credentials));
58
58
  if (missing.length > 0) {
59
- return `Missing credential field(s) for ${input.platform}: ${missing.join(', ')}. Call get_connect_link with platform=${input.platform} to see field descriptions.`;
59
+ return `Missing credential field(s) for ${input.platform}: ${missing.join(', ')}. Call list_platforms with view=connect_link and target=${input.platform} to see field descriptions.`;
60
60
  }
61
61
  const result = await client.connectAccountCredentials({
62
62
  platform: input.platform,
@@ -20,7 +20,7 @@ function formatCreatedApiKey(result) {
20
20
  }
21
21
  export const createApiKeyTool = {
22
22
  name: 'create_api_key',
23
- description: 'Create a new posterly API key for the authenticated user. SECRET-CREATING WRITE: only use after explicit user confirmation. The new key can only request scopes already present on the calling dashboard-created API key; OAuth and managed assistant tokens cannot mint keys.',
23
+ description: 'Create a new posterly API key for the authenticated user. SECRET-CREATING WRITE: only use after explicit user confirmation. The new key can only request scopes already present on the calling dashboard-created API key; OAuth and managed assistant tokens cannot mint keys. Team management (members:read, members:write) and client portal sign-in link permissions are never copied or granted here: a person turns them on in /dashboard/api.',
24
24
  inputSchema: z.object({
25
25
  name: z.string().trim().min(1).max(120).optional().describe('Human-readable key name.'),
26
26
  scopes: z.array(scopeSchema).min(1).max(8).optional().describe('Scopes for the new key. Omit to copy the calling key scopes. Cannot exceed the calling key scopes. billing:read and billing:write allow managing the posterly subscription (cancel/pause/resume/downgrade).'),
@@ -9,12 +9,12 @@ export declare const createConnectSessionTool: {
9
9
  auto_start: z.ZodOptional<z.ZodBoolean>;
10
10
  debug: z.ZodOptional<z.ZodBoolean>;
11
11
  }, "strip", z.ZodTypeAny, {
12
- platform: "meta" | "instagram_direct" | "instagram" | "instagram-standalone" | "facebook" | "tiktok" | "twitter" | "threads" | "linkedin" | "youtube" | "pinterest" | "google_business" | "medium" | "x" | "telegram" | "bluesky" | "discord" | "slack" | "mastodon" | "devto" | "hashnode" | "wordpress" | "lemmy" | "reddit" | "skool" | "whop" | "gmb" | "google-business" | "google_business_profile" | "linkedin_page" | "facebook_instagram" | "facebook_pages" | "linkedin-company" | "linkedin-page" | "linkedin_company" | "linkedin_personal" | "meta_business" | "x_twitter";
12
+ platform: "meta" | "instagram_direct" | "instagram" | "instagram-standalone" | "facebook" | "tiktok" | "twitter" | "threads" | "linkedin" | "youtube" | "pinterest" | "google_business" | "medium" | "x" | "telegram" | "bluesky" | "discord" | "wordpress" | "devto" | "hashnode" | "lemmy" | "slack" | "mastodon" | "reddit" | "skool" | "whop" | "gmb" | "google-business" | "google_business_profile" | "linkedin_page" | "facebook_instagram" | "facebook_pages" | "linkedin-company" | "linkedin-page" | "linkedin_company" | "linkedin_personal" | "meta_business" | "x_twitter";
13
13
  workspace_id?: string | undefined;
14
14
  auto_start?: boolean | undefined;
15
15
  debug?: boolean | undefined;
16
16
  }, {
17
- platform: "meta" | "instagram_direct" | "instagram" | "instagram-standalone" | "facebook" | "tiktok" | "twitter" | "threads" | "linkedin" | "youtube" | "pinterest" | "google_business" | "medium" | "x" | "telegram" | "bluesky" | "discord" | "slack" | "mastodon" | "devto" | "hashnode" | "wordpress" | "lemmy" | "reddit" | "skool" | "whop" | "gmb" | "google-business" | "google_business_profile" | "linkedin_page" | "facebook_instagram" | "facebook_pages" | "linkedin-company" | "linkedin-page" | "linkedin_company" | "linkedin_personal" | "meta_business" | "x_twitter";
17
+ platform: "meta" | "instagram_direct" | "instagram" | "instagram-standalone" | "facebook" | "tiktok" | "twitter" | "threads" | "linkedin" | "youtube" | "pinterest" | "google_business" | "medium" | "x" | "telegram" | "bluesky" | "discord" | "wordpress" | "devto" | "hashnode" | "lemmy" | "slack" | "mastodon" | "reddit" | "skool" | "whop" | "gmb" | "google-business" | "google_business_profile" | "linkedin_page" | "facebook_instagram" | "facebook_pages" | "linkedin-company" | "linkedin-page" | "linkedin_company" | "linkedin_personal" | "meta_business" | "x_twitter";
18
18
  workspace_id?: string | undefined;
19
19
  auto_start?: boolean | undefined;
20
20
  debug?: boolean | undefined;