hermoso 0.1.464 → 0.1.466
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/mcp/client.mjs +15 -2
- package/mcp/roster-scope.mjs +44 -2
- package/mcp/tools.mjs +1293 -1268
- package/package.json +1 -1
package/mcp/client.mjs
CHANGED
|
@@ -84,10 +84,23 @@ const scopedBrand = (p) => {
|
|
|
84
84
|
const b = brandScope.getStore()?.brand;
|
|
85
85
|
return typeof b === 'string' && b.trim() && !/^\/api\/apple-ads\//.test(String(p || '')) ? b.trim() : '';
|
|
86
86
|
};
|
|
87
|
+
// ── AN ACCOUNT NAMED ON ONE TOOL CALL SCOPES EVERY REQUEST THAT CALL MAKES (2026-10-29) ──────────────────────────
|
|
88
|
+
// A brand can hold several accounts of one channel (lib/multi-account.mjs), and the server refuses a write that names
|
|
89
|
+
// none: "2 youtube accounts are connected, name one with account: …". `update_youtube_video` had no `account`
|
|
90
|
+
// argument to name one with, so a customer with two channels could not flip a video public (fb_f6f7b0371d42). Same
|
|
91
|
+
// shape as the brand above: wrap() (tools.mjs) puts the caller's `account` in toolCtx for a tool whose provider is a
|
|
92
|
+
// multi-account channel, and it rides here as `?account=`, which the server's connCtxFor reads on every method.
|
|
93
|
+
// A body `account` the handler already sends is read FIRST by the server and is the same value.
|
|
94
|
+
const scopedAccount = () => { const a = toolCtx.getStore()?.account; return typeof a === 'string' && a.trim() ? a.trim() : ''; };
|
|
95
|
+
const withAccountScope = (p) => {
|
|
96
|
+
const a = scopedAccount();
|
|
97
|
+
if (!a || /[?&]account=/.test(String(p))) return p;
|
|
98
|
+
return `${p}${String(p).includes('?') ? '&' : '?'}account=${encodeURIComponent(a)}`;
|
|
99
|
+
};
|
|
87
100
|
export const withBrandScope = (p) => {
|
|
88
101
|
const b = scopedBrand(p);
|
|
89
|
-
if (!b || /[?&]brandId=/.test(String(p))) return p;
|
|
90
|
-
return `${p}${String(p).includes('?') ? '&' : '?'}brandId=${encodeURIComponent(b)}
|
|
102
|
+
if (!b || /[?&]brandId=/.test(String(p))) return withAccountScope(p);
|
|
103
|
+
return withAccountScope(`${p}${String(p).includes('?') ? '&' : '?'}brandId=${encodeURIComponent(b)}`);
|
|
91
104
|
};
|
|
92
105
|
|
|
93
106
|
function headers(extra = {}) {
|
package/mcp/roster-scope.mjs
CHANGED
|
@@ -111,7 +111,14 @@ export const TOOL_PROVIDER_RULES = [
|
|
|
111
111
|
[/onedrive/, 'microsoft_onedrive'],
|
|
112
112
|
[/bluesky/, 'bluesky'],
|
|
113
113
|
[/telegram/, 'telegram'],
|
|
114
|
-
|
|
114
|
+
// TIKTOK IS THREE CREDENTIALS, AND ONLY FIVE TOOLS RIDE THE POSTING ONE (2026-10-29). Until this date the bare rule sent
|
|
115
|
+
// every remaining tiktok name to 'tiktok', so 41 Business, analytics and creator-marketplace tools were offered by the
|
|
116
|
+
// POSTING login while their routes spend the TikTok Ads advertiser token or its account-holder grant: a TikTok Ads-only
|
|
117
|
+
// brand lost tools that work, a posting-only brand saw tools that 401. The posting family is named exactly; every
|
|
118
|
+
// other tiktok tool is the Ads connection (see TIKTOK_ACCOUNT_GRANT_TOOLS for the grant either row can host).
|
|
119
|
+
// tools/tiktok-roster-gate-check.mjs derives each tool's credential from server.js and fails when this disagrees.
|
|
120
|
+
[/^(post_to_tiktok|tiktok_post_status|tiktok_creator_info|tiktok_account|list_tiktok_videos)$/, 'tiktok'],
|
|
121
|
+
[/tiktok/, 'tiktok_ads'],
|
|
115
122
|
[/^post_to_x$|^delete_x_post$|^edit_x_post$|^post_x_article$|^send_x_dm$|^list_x_dms$|^search_x$|^list_x_blocks$|^hide_x_reply$|_x_broadcast|_x_chat|^x_chat_settings$|^x_(mentions|post|account|trends|user|search_counts|follows)/, 'x'],
|
|
116
123
|
[/pinterest/, 'pinterest'],
|
|
117
124
|
[/reddit/, 'reddit'],
|
|
@@ -128,6 +135,27 @@ export const TOOL_PROVIDER_RULES = [
|
|
|
128
135
|
// in every listing. If Reddit ever approves the app, remove it from this set and the whole surface returns.
|
|
129
136
|
export const UNOFFERED_PROVIDERS = new Set(['reddit', 'snapchat']);
|
|
130
137
|
export function toolUnoffered(name) { const p = toolProvider(name); return !!p && UNOFFERED_PROVIDERS.has(p); }
|
|
138
|
+
|
|
139
|
+
// ── WHICH TOOLS TAKE `account` (2026-10-29, customer bug fb_f6f7b0371d42) ─────────────────────────────────────────────
|
|
140
|
+
// A brand can connect several accounts of these channels (lib/multi-account.mjs), and the server refuses a write that
|
|
141
|
+
// names none. Only the post_to_* tools declared `account`, so a user with two YouTube channels was told "name one with
|
|
142
|
+
// account: …" by update_youtube_video, which had no such argument. Every surface (both MCP twins' registration seam,
|
|
143
|
+
// the Studio agent's schema and executor) asks THIS function, so the answer cannot differ between them.
|
|
144
|
+
// The set mirrors lib/multi-account.mjs MULTI_ACCOUNT_PROVIDERS, which the npm package cannot import;
|
|
145
|
+
// tools/multi-account-arg-check.mjs fails when the two differ, and DERIVES the tool split below from server.js.
|
|
146
|
+
export const MULTI_ACCOUNT_CHANNELS = Object.freeze(new Set(['tiktok', 'x', 'youtube', 'threads', 'bluesky', 'telegram', 'reddit', 'pinterest', 'instagram']));
|
|
147
|
+
// Filed under a multi-account channel, but routed through a connection that holds ONE grant per brand, so `account`
|
|
148
|
+
// would do nothing there: these two Pinterest reads ride the ad-account (pinterest_ads) row. (The TikTok Business and
|
|
149
|
+
// Ads tools are no longer filed under 'tiktok' at all — only the five posting tools are — so they need no entry.)
|
|
150
|
+
const SINGLE_GRANT_TOOLS = new Set(['pinterest_targeting_analytics', 'pinterest_audience_insights']);
|
|
151
|
+
export function accountScopedTool(name) {
|
|
152
|
+
const p = String(toolProvider(name) || '');
|
|
153
|
+
if (!MULTI_ACCOUNT_CHANNELS.has(p)) return false;
|
|
154
|
+
return !SINGLE_GRANT_TOOLS.has(String(name));
|
|
155
|
+
}
|
|
156
|
+
// The channel an account-scoped tool's `account` names, or null.
|
|
157
|
+
export const accountChannelOf = (name) => (accountScopedTool(name) ? String(toolProvider(name)) : null);
|
|
158
|
+
export const ACCOUNT_ARG_TEXT = 'which connected account: its @handle or id from list_connector_accounts; needed only when several are connected';
|
|
131
159
|
export function toolProvider(name) {
|
|
132
160
|
const n = String(name || '');
|
|
133
161
|
if (!n) return null;
|
|
@@ -161,12 +189,25 @@ export const INSTAGRAM_LOGIN_TOOLS = new Set([
|
|
|
161
189
|
// row whose business token `waToken` resolves FIRST, with no Meta connection at all — holding its WhatsApp tools back
|
|
162
190
|
// as "needs meta" would refuse a working connection. Nobody needs BOTH for one account, on either channel.
|
|
163
191
|
export const isWhatsAppTool = (name) => /whatsapp/.test(String(name || ''));
|
|
192
|
+
// THE SAME SHAPE FOR TIKTOK'S ACCOUNT-HOLDER GRANT (2026-10-29). These tools map to 'tiktok_ads' and spend the TikTok
|
|
193
|
+
// ACCOUNT authorization (server.js ttAccountGrant), which lives on the TikTok Ads row when there is one and otherwise on
|
|
194
|
+
// the TikTok posting row (lib/tiktok-account.mjs TIKTOK_ACCOUNT_HOSTS: a personal creator with no ad account). So either
|
|
195
|
+
// row is enough to offer them. tools/tiktok-roster-gate-check.mjs derives this set from server.js and fails on drift.
|
|
196
|
+
export const TIKTOK_ACCOUNT_GRANT_TOOLS = new Set([
|
|
197
|
+
'comment_on_tiktok_video', 'list_tiktok_comments', 'list_tiktok_comment_replies', 'reply_to_tiktok_comment', 'moderate_tiktok_comment', 'upload_tiktok_comment_image',
|
|
198
|
+
'list_tiktok_mentions', 'get_tiktok_mention', 'list_tiktok_mention_comments', 'tiktok_mention_top_terms',
|
|
199
|
+
'list_tiktok_brand_hashtags', 'list_tiktok_brand_hashtag_posts', 'manage_tiktok_brand_hashtags', 'tiktok_hashtag_suggestions',
|
|
200
|
+
'get_tiktok_post_ad_authorization', 'set_tiktok_post_ad_authorization', 'extend_tiktok_post_ad_authorization', 'delete_tiktok_post_ad_authorization',
|
|
201
|
+
'tiktok_account_insights', 'tiktok_account_status', 'tiktok_video_insights', 'tiktok_category_benchmark', 'tiktok_spark_recommendations', 'tiktok_trends',
|
|
202
|
+
]);
|
|
164
203
|
// The ONE sentence every surface appends when a 'meta'-mapped tool is held and an alternative connection exists.
|
|
165
204
|
export const metaAlternativeNote = (name) => INSTAGRAM_LOGIN_TOOLS.has(String(name || ''))
|
|
166
205
|
? ' For an Instagram account with no Facebook Page, the "instagram" connection alone is enough for this tool; "meta" covers an Instagram account linked to a Page (and ads). One account never needs both.'
|
|
167
206
|
: isWhatsAppTool(name)
|
|
168
207
|
? ' A WhatsApp Business Account the business already manages is ticked on the "meta" connection\'s assets step; the "whatsapp" connection sets up a number the business does not have yet. Either one is enough for this tool.'
|
|
169
|
-
: ''
|
|
208
|
+
: TIKTOK_ACCOUNT_GRANT_TOOLS.has(String(name || ''))
|
|
209
|
+
? ' A creator with no TikTok ad account can use the "tiktok" connection instead; either one, plus authorizing the TikTok account on its card, is enough for this tool.'
|
|
210
|
+
: '';
|
|
170
211
|
// ── A TOOL THE SERVER WITHHOLDS FROM EVERY ROSTER (2026-09-27, owner: "why do we even allow access to creator
|
|
171
212
|
// marketplace if we can't even access it ourselves?"). `withheld` rides the same /api/connectors/providers read as the
|
|
172
213
|
// connected set: the server names the tools whose capability it does not offer right now (today: the Instagram
|
|
@@ -195,5 +236,6 @@ export function toolHeldBackByConnectors(name, conn) {
|
|
|
195
236
|
const on = conn.connected instanceof Set ? conn.connected : new Set(conn.connected || []);
|
|
196
237
|
if (p === 'meta' && on.has('instagram') && INSTAGRAM_LOGIN_TOOLS.has(name)) return false;
|
|
197
238
|
if (p === 'meta' && on.has('whatsapp') && isWhatsAppTool(name)) return false;
|
|
239
|
+
if (p === 'tiktok_ads' && on.has('tiktok') && TIKTOK_ACCOUNT_GRANT_TOOLS.has(name)) return false;
|
|
198
240
|
return !on.has(p);
|
|
199
241
|
}
|