hermoso 0.1.449 → 0.1.451
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/arg-guard.mjs +112 -0
- package/mcp/client.mjs +1 -1
- package/mcp/tools.mjs +51 -3
- package/package.json +1 -1
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// AN ARGUMENT A TOOL DOES NOT TAKE IS NEVER SILENTLY DROPPED (2026-10-26, customer report).
|
|
2
|
+
//
|
|
3
|
+
// meta_post_insights was called with `platform: 'instagram'` instead of `target`. The MCP SDK parses arguments with
|
|
4
|
+
// the tool's zod object, which STRIPS unknown keys, so the handler never saw `platform`, defaulted to Facebook and read
|
|
5
|
+
// Facebook metrics for an Instagram media — a confident wrong answer, with nothing saying an argument was ignored.
|
|
6
|
+
// The same strip happens on /v1 (z.object) and the Studio agent reads `input.<name>` and nothing else.
|
|
7
|
+
//
|
|
8
|
+
// ONE RULE FOR EVERY SURFACE, applied where each surface parses: an argument the schema does not declare is named
|
|
9
|
+
// back to the caller, with the declared argument it most likely meant (modes: argGuardMode). The default refuses only a
|
|
10
|
+
// probable typo/synonym of a real argument and otherwise runs the call with the note first in its answer, because hosts
|
|
11
|
+
// and agent frameworks were never measured for what they add to `arguments` (the advertised schemas already say
|
|
12
|
+
// additionalProperties:false, and call_tool has refused every undeclared key since 2026-09-30). Every unknown key is
|
|
13
|
+
// logged by NAME (never value) so the blast radius of full `reject` is measured from production before anyone flips it.
|
|
14
|
+
// Keys starting with `_` are host metadata (`_meta`, `_hermoso…`) and are never named.
|
|
15
|
+
//
|
|
16
|
+
// Byte-identical in mcp/ and cli/mcp/ (the CLI ships rooted at cli/), and imported by server.js for /v1 and the Studio.
|
|
17
|
+
|
|
18
|
+
export const UNKNOWN_ARGS_SYM = Symbol.for('hermoso.unknownArgs');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 'typo' (default) | 'reject' | 'warn' | 'off' from the environment value. Anything unrecognised is `typo`:
|
|
22
|
+
* typo — REFUSED when an unknown key is a likely misspelling or synonym of a declared one (the customer's
|
|
23
|
+
* `platform` for `target`: running without it gives a confidently wrong answer); otherwise the call runs
|
|
24
|
+
* and its answer starts with the note;
|
|
25
|
+
* reject — every unknown key refuses the call (what call_tool has done since 2026-09-30);
|
|
26
|
+
* warn — never refuses, always notes; off — the old silent strip.
|
|
27
|
+
*/
|
|
28
|
+
export function argGuardMode(v = (typeof process !== 'undefined' ? process.env?.HERMOSO_UNKNOWN_ARGS : '')) {
|
|
29
|
+
const s = String(v || '').trim().toLowerCase();
|
|
30
|
+
return s === 'reject' || s === 'off' || s === 'warn' ? s : 'typo';
|
|
31
|
+
}
|
|
32
|
+
/** Whether this mode refuses this set of unknown keys. */
|
|
33
|
+
export function argGuardRefuses(mode, unknown = [], declared = []) {
|
|
34
|
+
if (!unknown.length || mode === 'off' || mode === 'warn') return false;
|
|
35
|
+
if (mode === 'reject') return true;
|
|
36
|
+
return unknown.some((k) => !!didYouMean(k, declared));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The argument names a schema declares, or null when it accepts any key (so nothing can be called unknown).
|
|
41
|
+
* Accepts a zod raw shape ({name: zodType}), a zod object, or a JSON Schema object (the Studio's input_schema).
|
|
42
|
+
*/
|
|
43
|
+
export function declaredArgKeys(schema) {
|
|
44
|
+
if (!schema || typeof schema !== 'object') return null;
|
|
45
|
+
// JSON Schema (checked AFTER zod: a zod v4 object also carries `type: 'object'`)
|
|
46
|
+
if (typeof schema.safeParse !== 'function' && (schema.type === 'object' || (schema.properties && typeof schema.properties === 'object'))) {
|
|
47
|
+
if (schema.additionalProperties === true || (schema.additionalProperties && typeof schema.additionalProperties === 'object')) return null;
|
|
48
|
+
return Object.keys(schema.properties || {});
|
|
49
|
+
}
|
|
50
|
+
// zod object
|
|
51
|
+
if (typeof schema.safeParse === 'function') {
|
|
52
|
+
const def = schema._zod?.def || schema._def || {};
|
|
53
|
+
const shape = typeof schema.shape === 'object' ? schema.shape : (typeof def.shape === 'function' ? def.shape() : def.shape);
|
|
54
|
+
if (!shape || typeof shape !== 'object') return null;
|
|
55
|
+
const cat = def.catchall;
|
|
56
|
+
const loose = (cat && (cat._zod?.def?.type || cat._def?.typeName) && !/never/i.test(String(cat._zod?.def?.type || cat._def?.typeName))) || def.unknownKeys === 'passthrough';
|
|
57
|
+
return loose ? null : Object.keys(shape);
|
|
58
|
+
}
|
|
59
|
+
// raw shape: every value is a zod type
|
|
60
|
+
const vals = Object.values(schema);
|
|
61
|
+
if (vals.length && vals.every((v) => v && typeof v === 'object' && typeof v.safeParse === 'function')) return Object.keys(schema);
|
|
62
|
+
if (!vals.length) return [];
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The keys of `args` the schema does not declare (host `_` metadata excluded). [] when there are none or none can be judged. */
|
|
67
|
+
export function unknownArgs(declared, args) {
|
|
68
|
+
if (!Array.isArray(declared) || !args || typeof args !== 'object' || Array.isArray(args)) return [];
|
|
69
|
+
const known = new Set(declared);
|
|
70
|
+
return Object.keys(args).filter((k) => !known.has(k) && !k.startsWith('_'));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function lev(a, b) {
|
|
74
|
+
a = a.toLowerCase(); b = b.toLowerCase();
|
|
75
|
+
const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
76
|
+
for (let j = 1; j <= b.length; j++) d[0][j] = j;
|
|
77
|
+
for (let i = 1; i <= a.length; i++) for (let j = 1; j <= b.length; j++) d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
78
|
+
return d[a.length][b.length];
|
|
79
|
+
}
|
|
80
|
+
// Words agents reach for that name the same idea under a different argument (the customer's case is the first row).
|
|
81
|
+
const ALIASES = {
|
|
82
|
+
platform: ['target', 'platforms', 'channel', 'channels', 'network', 'source', 'sources'],
|
|
83
|
+
network: ['platform', 'target', 'channel'],
|
|
84
|
+
channel: ['channels', 'platform', 'target'],
|
|
85
|
+
count: ['limit', 'max', 'n'], max: ['limit'], max_results: ['limit'], maxResults: ['limit'], page_size: ['limit'], pageSize: ['limit'],
|
|
86
|
+
page: ['cursor'], next: ['cursor'], nextCursor: ['cursor'], after: ['cursor'],
|
|
87
|
+
text: ['message', 'caption', 'body'], message: ['text', 'caption'], caption: ['message', 'text'],
|
|
88
|
+
url: ['imageUrl', 'videoUrl', 'link', 'image', 'video'], image: ['imageUrl'], video: ['videoUrl'],
|
|
89
|
+
id: ['postId', 'mediaId', 'commentId', 'videoId', 'conversationId'],
|
|
90
|
+
};
|
|
91
|
+
/** The declared argument `key` most likely meant, or ''. */
|
|
92
|
+
export function didYouMean(key, declared = []) {
|
|
93
|
+
const k = String(key);
|
|
94
|
+
const alias = (ALIASES[k] || []).find((a) => declared.includes(a));
|
|
95
|
+
if (alias) return alias;
|
|
96
|
+
const lower = declared.find((d) => d.toLowerCase() === k.toLowerCase() || d.toLowerCase().replace(/_/g, '') === k.toLowerCase().replace(/_/g, ''));
|
|
97
|
+
if (lower) return lower;
|
|
98
|
+
if (/id$/i.test(k) && k.length > 2 && declared.includes('id') && !declared.some((d) => /id$/i.test(d) && d !== 'id')) return 'id'; // jobId / job_id for a tool whose only id is `id`
|
|
99
|
+
let best = '', bd = Infinity;
|
|
100
|
+
for (const d of declared) { const x = lev(k, d); if (x < bd) { bd = x; best = d; } }
|
|
101
|
+
return bd <= Math.max(2, Math.floor(k.length / 3)) ? best : '';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The one sentence every surface prints. `rejected` words it as a refusal; otherwise as a note on a call that ran. */
|
|
105
|
+
export function unknownArgsNote(tool, unknown = [], declared = [], { rejected = false } = {}) {
|
|
106
|
+
if (!unknown.length) return '';
|
|
107
|
+
const named = unknown.map((k) => { const m = didYouMean(k, declared); return `\`${k}\`${m ? ` (did you mean \`${m}\`?)` : ''}`; }).join(', ');
|
|
108
|
+
const takes = declared.length ? ` ${tool || 'This tool'} takes: ${declared.join(', ')}.` : ` ${tool || 'This tool'} takes no arguments.`;
|
|
109
|
+
return rejected
|
|
110
|
+
? `Unknown argument${unknown.length > 1 ? 's' : ''} ${named} for ${tool || 'this tool'} — nothing was run.${takes}`
|
|
111
|
+
: `⚠ Ignored unknown argument${unknown.length > 1 ? 's' : ''} ${named}: ${tool || 'this tool'} does not take ${unknown.length > 1 ? 'them' : 'it'}, so the call ran WITHOUT ${unknown.length > 1 ? 'them' : 'it'}. Check the answer below still matches what you asked, or call again with the right name.${takes}`;
|
|
112
|
+
}
|
package/mcp/client.mjs
CHANGED
|
@@ -146,7 +146,7 @@ async function unwrap(res, { read = false } = {}) {
|
|
|
146
146
|
// ledger with the tool name off x-hermoso-tool. wrap() reports ONLY the errors that lack this marker — a local
|
|
147
147
|
// throw, a schema rejection, a socket reset — which is what stops the twins double-counting every 4xx.
|
|
148
148
|
// `connectUrl` rides a not-connected 401 with the brand already in it, so a hint never rebuilds the link.
|
|
149
|
-
throw Object.assign(new Error(msg), { status: res.status, _viaApi: true, ...(body?.connector ? { connector: body.connector } : {}), ...(typeof body?.connectUrl === 'string' ? { connectUrl: body.connectUrl } : {}), ...(body?.meta?.videoChoice && typeof body.meta.videoChoice === 'object' ? { videoChoice: body.meta.videoChoice } : {}), ...(body?.metaAuthHold === true ? { metaAuthHold: true } : {}), ...(body?.meta?.welcomeClaim && typeof body.meta.welcomeClaim === 'object' && body.meta.welcomeClaim.url ? { welcomeClaim: { credits: Number(body.meta.welcomeClaim.credits) || 0, url: String(body.meta.welcomeClaim.url) } } : {}), ...(body?.remedy && typeof body.remedy === 'object' && typeof body.remedy.code === 'string' ? { remedy: body.remedy } : {}) }); // `remedy` (2026-10-25): the server's structured next step (a publish outcome, Meta's security hold), handed on whole to wrap()'s _meta and /v1 // `videoChoice` rides a 402 for a video the caller expects and cannot afford (server videoChoiceFor) — wrap() spells its options out instead of a bare top-up line
|
|
149
|
+
throw Object.assign(new Error(msg), { status: res.status, _viaApi: true, ...(typeof body?.code === 'string' ? { apiCode: body.code } : {}), ...(body?.connector ? { connector: body.connector } : {}), ...(typeof body?.connectUrl === 'string' ? { connectUrl: body.connectUrl } : {}), ...(body?.meta?.videoChoice && typeof body.meta.videoChoice === 'object' ? { videoChoice: body.meta.videoChoice } : {}), ...(body?.metaAuthHold === true ? { metaAuthHold: true } : {}), ...(body?.meta?.welcomeClaim && typeof body.meta.welcomeClaim === 'object' && body.meta.welcomeClaim.url ? { welcomeClaim: { credits: Number(body.meta.welcomeClaim.credits) || 0, url: String(body.meta.welcomeClaim.url) } } : {}), ...(body?.remedy && typeof body.remedy === 'object' && typeof body.remedy.code === 'string' ? { remedy: body.remedy } : {}) }); // `remedy` (2026-10-25): the server's structured next step (a publish outcome, Meta's security hold), handed on whole to wrap()'s _meta and /v1 // `videoChoice` rides a 402 for a video the caller expects and cannot afford (server videoChoiceFor) — wrap() spells its options out instead of a bare top-up line
|
|
150
150
|
}
|
|
151
151
|
// WHAT THIS CALL WAS CHARGED (2026-10-19). A paid route answers {data, creditsUsed}, and the unwrap below drops the
|
|
152
152
|
// sibling, so a synchronous paid tool's reply could never say what it cost. It is added to the running tool call's
|
package/mcp/tools.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
12
12
|
// The UTF-16 well-formedness law — see wellFormedServer() below for why it is applied here, and
|
|
13
13
|
// ./well-formed.mjs for why that specifier is the only one that can work in a byte-identical twin.
|
|
14
14
|
import { wellFormedValue, wellFormedString } from './well-formed.mjs';
|
|
15
|
+
import { UNKNOWN_ARGS_SYM, argGuardMode, argGuardRefuses, declaredArgKeys, didYouMean, unknownArgs, unknownArgsNote } from './arg-guard.mjs'; // an undeclared argument is named back, never silently stripped (2026-10-26)
|
|
15
16
|
// WHICH CONNECTOR A TOOL NEEDS — the same table and the same decision the Studio chat applies (lib/studio-roster.mjs
|
|
16
17
|
// re-exports every symbol from here). `./roster-scope.mjs` is the only specifier that resolves in a byte-identical
|
|
17
18
|
// twin, for the same reason ./well-formed.mjs is. See applyToolGates() for the seam and roster-scope.mjs for the law.
|
|
@@ -2085,6 +2086,15 @@ export function inboxReplyTarget(id) {
|
|
|
2085
2086
|
* eight vendors disagree about all of them; what comes OUT is always the same, so an agent writes one loop.
|
|
2086
2087
|
* Returns null when there is no id or no text — an item you can neither quote nor answer is noise.
|
|
2087
2088
|
*/
|
|
2089
|
+
// THE SENTENCE A PARTLY-READ SOURCE CONTRIBUTES TO list_inbox `notes`, or '' when it read whole. A route reports a
|
|
2090
|
+
// part it could not read as `unreadable: [...]` beside its own `note` (the Meta DM inbox, the conversation list); the
|
|
2091
|
+
// note is the route's words, and the label says which source they belong to. PURE, so a check runs it.
|
|
2092
|
+
export function inboxShortNote(src, d) {
|
|
2093
|
+
const bad = Array.isArray(d?.unreadable) ? d.unreadable : [];
|
|
2094
|
+
if (!bad.length) return '';
|
|
2095
|
+
const said = typeof d?.note === 'string' && d.note.trim() ? d.note.trim() : bad.map((u) => String(u?.error || 'could not be read')).join('; ');
|
|
2096
|
+
return `${src?.label || 'A source'}s were not read in full: ${said.slice(0, 600)}`;
|
|
2097
|
+
}
|
|
2088
2098
|
export function normalizeInboxItem(source, row = {}) {
|
|
2089
2099
|
const src = BY_SOURCE.get(source);
|
|
2090
2100
|
if (!src) return null;
|
|
@@ -2805,7 +2815,7 @@ export const undeclaredArgsText = (name, extra, h) => {
|
|
|
2805
2815
|
const shape = h && h.inputSchema && h.inputSchema.shape;
|
|
2806
2816
|
const takes = shape && typeof shape === 'object' ? Object.keys(shape) : [];
|
|
2807
2817
|
const brandNote = extra.includes('brand') ? ` ${name} acts on the profile this connection is on — pin another with use_brand first, then call it again.` : '';
|
|
2808
|
-
return `${name} does not take ${extra.map((k) => `\`${k}
|
|
2818
|
+
return `${name} does not take ${extra.map((k) => { const m = didYouMean(k, takes); return `\`${k}\`${m ? ` (did you mean \`${m}\`?)` : ''}`; }).join(', ')}, so nothing was run: an argument a tool does not declare would be silently ignored.${brandNote} It takes: ${takes.length ? takes.join(', ') : '(no arguments)'}.`;
|
|
2809
2819
|
};
|
|
2810
2820
|
export async function legacyToolAnswer(name, request, extra, ctx) {
|
|
2811
2821
|
const spec = LEGACY_TOOL_NAMES[name];
|
|
@@ -3574,14 +3584,43 @@ function registerWidgetHostSplits(rawServer, ctx, opts) {
|
|
|
3574
3584
|
const _wfWrapped = new WeakMap();
|
|
3575
3585
|
// EXPORTED so tools/well-formed-law-check.mjs can RUN the real seam over real fixtures rather than read it. It is
|
|
3576
3586
|
// not part of the module's public surface in any other sense — `registerTools` is the only caller.
|
|
3587
|
+
// UNKNOWN ARGUMENTS ON A HOST'S OWN tools/call (2026-10-26, customer report — see mcp/arg-guard.mjs). The SDK parses
|
|
3588
|
+
// `arguments` with the tool's zod object, which strips an undeclared key before any handler sees it, so the only place
|
|
3589
|
+
// the raw keys exist is the SDK's validateToolInput. It is wrapped ONCE per server instance: a refusal throws (the SDK
|
|
3590
|
+
// turns that into an isError result), and a key that is noted rather than refused rides the parsed args under a
|
|
3591
|
+
// symbol until withUnknownArgsNote puts the sentence at the top of the answer.
|
|
3592
|
+
export function guardUnknownArgs(server) {
|
|
3593
|
+
if (!server || typeof server.validateToolInput !== 'function' || server[UNKNOWN_ARGS_SYM]) return server;
|
|
3594
|
+
const orig = server.validateToolInput;
|
|
3595
|
+
const guarded = async function (tool, args, toolName) {
|
|
3596
|
+
const declared = declaredArgKeys(tool?.inputSchema);
|
|
3597
|
+
const unknown = unknownArgs(declared, args);
|
|
3598
|
+
const mode = argGuardMode();
|
|
3599
|
+
if (unknown.length && mode !== 'off') {
|
|
3600
|
+
try { console.error(`[mcp-unknown-arg] tool=${toolName} keys=${unknown.join(',')} mode=${mode}`); } catch {}
|
|
3601
|
+
if (argGuardRefuses(mode, unknown, declared)) throw new Error(unknownArgsNote(toolName, unknown, declared, { rejected: true }));
|
|
3602
|
+
}
|
|
3603
|
+
const out = await orig.call(this, tool, args, toolName);
|
|
3604
|
+
if (unknown.length && mode !== 'off' && out && typeof out === 'object') { try { Object.defineProperty(out, UNKNOWN_ARGS_SYM, { value: unknownArgsNote(toolName, unknown, declared), enumerable: false, configurable: true }); } catch {} }
|
|
3605
|
+
return out;
|
|
3606
|
+
};
|
|
3607
|
+
try { Object.defineProperty(server, 'validateToolInput', { value: guarded, configurable: true, writable: true }); Object.defineProperty(server, UNKNOWN_ARGS_SYM, { value: true }); } catch {}
|
|
3608
|
+
return server;
|
|
3609
|
+
}
|
|
3610
|
+
export function withUnknownArgsNote(result, args) {
|
|
3611
|
+
const note = args && typeof args === 'object' ? args[UNKNOWN_ARGS_SYM] : '';
|
|
3612
|
+
if (!note || !result || typeof result !== 'object' || !Array.isArray(result.content)) return result;
|
|
3613
|
+
return { ...result, content: [{ type: 'text', text: note }, ...result.content] };
|
|
3614
|
+
}
|
|
3577
3615
|
export function wellFormedServer(rawServer) {
|
|
3616
|
+
guardUnknownArgs(rawServer);
|
|
3578
3617
|
return new Proxy(rawServer, {
|
|
3579
3618
|
get(t, p) {
|
|
3580
3619
|
if (p === 'registerTool') return (name, def, handler) => {
|
|
3581
3620
|
let safe = _wfWrapped.get(handler);
|
|
3582
3621
|
if (!safe) {
|
|
3583
3622
|
safe = async (args, extra) => {
|
|
3584
|
-
try { return wellFormedValue(await handler(args, extra)); }
|
|
3623
|
+
try { return wellFormedValue(withUnknownArgsNote(await handler(args, extra), args)); }
|
|
3585
3624
|
catch (e) {
|
|
3586
3625
|
try { const m = wellFormedString(e?.message); if (e && m !== e.message) e.message = m; } catch {}
|
|
3587
3626
|
throw e;
|
|
@@ -4477,6 +4516,12 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
4477
4516
|
try {
|
|
4478
4517
|
const d = await apiGet(whole ? src.readAll : src.read, q);
|
|
4479
4518
|
const rows = Array.isArray(d) ? d : (d.comments || d.replies || d.mentions || d.reviews || d.messages || d.events || d.items || d.data || []);
|
|
4519
|
+
// A SOURCE THAT READ ONLY PART OF ITSELF SAYS SO (2026-10-26, customer report: Instagram DMs at limit >= 32 came
|
|
4520
|
+
// back as 0 items and NO note, because the DM inbox route reported its failure in `unreadable` + `note` and this
|
|
4521
|
+
// loop only ever read the rows). Any route answering `unreadable` has its sentence carried into `notes`, on both
|
|
4522
|
+
// branches, so a short list is never mistaken for a quiet inbox.
|
|
4523
|
+
const short = inboxShortNote(src, d);
|
|
4524
|
+
if (short) notes.push(short);
|
|
4480
4525
|
if (whole) { for (const r of rows) { const it = normalizeInboxItem(src.source, r); if (it) items.push(it); } continue; } // already one row per conversation
|
|
4481
4526
|
// A DM FOLDS TO ONE ITEM AND CARRIES THE ID YOU ANSWER TO — see foldConversationRows. Deriving this from
|
|
4482
4527
|
// `kind` rather than naming the two sources means a third DM channel inherits it by declaring its kind.
|
|
@@ -4488,6 +4533,9 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
4488
4533
|
const use = src.dmReplyId ? foldConversationRows(rows, d?.[src.dmReplyId]) : rows;
|
|
4489
4534
|
for (const r of use) { const it = normalizeInboxItem(src.source, src.dmReplyId && typeof d?.conversationId === 'string' && !r.conversationId ? { ...r, conversationId: d.conversationId } : r); if (it) items.push(it); }
|
|
4490
4535
|
} catch (e) {
|
|
4536
|
+
// AN ID THAT DOES NOT RESOLVE ENDS THE CALL AS AN ERROR (2026-10-26, customer report: an unconnected pageId
|
|
4537
|
+
// answered "0 items"). The caller named a Page; reading nothing from it is not a quiet inbox, it is a wrong id.
|
|
4538
|
+
if (e?.apiCode === 'meta_page_not_connected') return { content: [{ type: 'text', text: String(e.message) }], isError: true };
|
|
4491
4539
|
// NAMED, never swallowed. A connector that is simply not linked is not worth a line; anything else is.
|
|
4492
4540
|
const msg = String(e?.message || e);
|
|
4493
4541
|
if (e?.status === 401 || /not connected/i.test(msg)) continue;
|
|
@@ -5057,7 +5105,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
5057
5105
|
inputSchema: {
|
|
5058
5106
|
...MANAGE_BRAND,
|
|
5059
5107
|
postId: z.string().describe('post/media id returned by post_to_meta'),
|
|
5060
|
-
target: z.enum(['facebook', 'instagram']).optional().describe('which metric set to ask for (default
|
|
5108
|
+
target: z.enum(['facebook', 'instagram']).optional().describe('which metric set to ask for; an Instagram media id is detected and read as Instagram either way (default: Facebook for a Facebook id)'),
|
|
5061
5109
|
account: z.string().optional().describe('which Instagram account — an @handle or id from list_connector_accounts("instagram"). Needed when several are linked, and the way an Instagram Login (standalone) account is reached; omit for the one Page-linked account.'),
|
|
5062
5110
|
pageId: z.string().optional().describe('Page id — omit when only one Page is connected'),
|
|
5063
5111
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.451",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
5
|
"description": "AI marketing agent and AI ad generator, run from your own AI agent. 933 tools. No markup on models. Publishing, scheduling, ad campaign management, comments, DMs and analytics are free on every plan. AD PLATFORMS: Meta, Google Ads, TikTok Ads, LinkedIn Ads, Reddit Ads, X Ads, Pinterest Ads, Snapchat Ads, Microsoft Advertising, Apple Search Ads, ChatGPT Ads and AppLovin Ads, plus product feeds in Google Merchant Center. PUBLISHING AND SCHEDULING: Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn, Pinterest, Bluesky, Telegram and Google Business Profile. INSTAGRAM DM AUTOMATIONS: a keyword comment, a DM, a story reply or an ig.me link gets an automatic private reply from the brand. AD RESEARCH: the Meta, Google and LinkedIn ad libraries plus organic TikTok, Instagram, YouTube, Threads and Reddit. ANALYTICS: Google Analytics 4, Google Search Console and every connected platform's own post and campaign insights. Also brand onboarding, 50+ image and video generation models, ad scoring, competitor teardowns, Google Drive and OneDrive, a CLI and installable Claude skills.",
|
|
6
6
|
"type": "module",
|