@rcs-campaigns/agent-toolkit 0.1.9 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,14 +24,18 @@ key** (Admin only). Pick the scopes the agent needs, then **copy the key — it
24
24
  shown exactly once** (`rcsk_live_…`). Treat it like a password; you can
25
25
  revoke/rotate it from the same screen.
26
26
 
27
- Scopes: `templates:read`, `templates:write`, `templates:submit`,
27
+ Scopes: `campaigns:read`, `campaigns:write`, `campaigns:submit`,
28
28
  `marketplace:clone`, `messages:send`, `usage:read`, `surveys:read`,
29
29
  `surveys:write`, `surveys:submit`, `surveys:send`, `results:read`.
30
30
 
31
- ### 2. Set the gateway URL
31
+ ### 2. Gateway URL (optional)
32
+
33
+ The toolkit talks to the hosted production gateway by default, so a production
34
+ key needs nothing else. Set `RCS_GATEWAY_URL` (or pass `--url`) only to target
35
+ dev or a local gateway:
32
36
 
33
37
  ```bash
34
- RCS_GATEWAY_URL=https://rcs-templates-api-dot-msg2ai-server.ue.r.appspot.com
38
+ RCS_GATEWAY_URL=http://localhost:3002
35
39
  ```
36
40
 
37
41
  > The key is tied to the environment it was minted in — a key from one
@@ -67,7 +71,6 @@ pnpm --filter @rcs-campaigns/agent-toolkit build # -> dist/mcp.js, dist/cli.js
67
71
  ### 4. Verify the key works (before wiring up a host)
68
72
 
69
73
  ```bash
70
- export RCS_GATEWAY_URL=https://rcs-templates-api-dot-msg2ai-server.ue.r.appspot.com
71
74
  export RCS_AGENT_KEY=rcsk_live_…
72
75
  npx -y -p @rcs-campaigns/agent-toolkit rcs capabilities
73
76
  ```
@@ -88,7 +91,6 @@ name.
88
91
 
89
92
  ```bash
90
93
  claude mcp add rcs-campaigns \
91
- --env RCS_GATEWAY_URL=https://rcs-templates-api-dot-msg2ai-server.ue.r.appspot.com \
92
94
  --env RCS_AGENT_KEY=rcsk_live_… \
93
95
  -- npx -y -p @rcs-campaigns/agent-toolkit rcs-mcp
94
96
  ```
@@ -110,7 +112,6 @@ Edit `claude_desktop_config.json`
110
112
  "command": "npx",
111
113
  "args": ["-y", "-p", "@rcs-campaigns/agent-toolkit", "rcs-mcp"],
112
114
  "env": {
113
- "RCS_GATEWAY_URL": "https://rcs-templates-api-dot-msg2ai-server.ue.r.appspot.com",
114
115
  "RCS_AGENT_KEY": "rcsk_live_…"
115
116
  }
116
117
  }
@@ -160,7 +161,7 @@ out — pipe into `jq`, wire into CI, or call from a skill.
160
161
  | Symptom | Cause / fix |
161
162
  |---|---|
162
163
  | `No agent key configured` | `RCS_AGENT_KEY` not reaching the process — for MCP hosts it must be in the server's `env` block, not your shell. |
163
- | `Gateway 401` | Key wrong, revoked, or minted in a different environment than `RCS_GATEWAY_URL` points at. |
164
+ | `Gateway 401` | Key wrong, revoked, or minted in a different environment than the gateway you're calling (production unless `RCS_GATEWAY_URL` says otherwise). |
164
165
  | `Gateway 403` (scope) | The key lacks the tool's scope — mint a new key with it (scopes are fixed at creation). |
165
166
  | Host shows no tools | Wrong path to `dist/mcp.js`, or the package wasn't built (`pnpm --filter @rcs-campaigns/agent-toolkit build`). |
166
167
  | Empty search results | The org has no published/owned templates yet — clone one from the marketplace first. |
@@ -172,7 +173,7 @@ out — pipe into `jq`, wire into CI, or call from a skill.
172
173
  ```ts
173
174
  import { GatewayClient, TOOLS } from '@rcs-campaigns/agent-toolkit';
174
175
 
175
- const client = GatewayClient.fromEnv(); // reads RCS_GATEWAY_URL + RCS_AGENT_KEY
176
+ const client = GatewayClient.fromEnv(); // reads RCS_AGENT_KEY (+ optional RCS_GATEWAY_URL)
176
177
  const { items } = await client.call({
177
178
  method: 'GET',
178
179
  path: '/templates',
package/dist/cli.js CHANGED
@@ -12,7 +12,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  * org's own brand) and reads RCS_*. Either prefix is accepted under either bin.
13
13
  *
14
14
  * Config (flags override env):
15
- * --url <gateway base url> | WA_GATEWAY_URL / RCS_GATEWAY_URL
15
+ * --url <gateway base url> | WA_GATEWAY_URL / RCS_GATEWAY_URL (optional;
16
+ * defaults to the hosted production gateway)
16
17
  * --key <rcsk_…> | WA_AGENT_KEY / RCS_AGENT_KEY
17
18
  * --channel RCS|WHATSAPP | WA_DEFAULT_CHANNEL / RCS_DEFAULT_CHANNEL
18
19
  *
@@ -29,30 +30,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
29
30
  */
30
31
  const node_fs_1 = require("node:fs");
31
32
  const client_1 = require("./client");
32
- /** Minimal argv parser: --flag value, --bool, and repeatable --to/--var. */
33
- function parseArgs(argv) {
34
- const out = { _: [], flags: {}, multi: { to: [], var: [], audience: [] } };
35
- for (let i = 0; i < argv.length; i++) {
36
- const a = argv[i];
37
- if (a.startsWith('--')) {
38
- const key = a.slice(2);
39
- const next = argv[i + 1];
40
- const takesValue = next !== undefined && !next.startsWith('--');
41
- const value = takesValue ? argv[++i] : true;
42
- if (key === 'to' || key === 'var' || key === 'audience') {
43
- if (typeof value === 'string')
44
- out.multi[key].push(value);
45
- }
46
- else {
47
- out.flags[key] = value;
48
- }
49
- }
50
- else {
51
- out._.push(a);
52
- }
53
- }
54
- return out;
55
- }
33
+ const tools_1 = require("./tools");
34
+ const cliDispatch_1 = require("./cliDispatch");
56
35
  /**
57
36
  * Read a JSON body from a file, letting the skin's default channel apply when
58
37
  * the document doesn't name one. An explicit `channel` in the file always wins,
@@ -83,11 +62,28 @@ function kvPairs(pairs) {
83
62
  function print(data) {
84
63
  process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
85
64
  }
65
+ // Catalog dispatch: everything without a curated command below is reached
66
+ // through the tool catalog, so a new gateway capability is callable from the
67
+ // CLI as soon as it has a `tools.ts` entry. The parsing lives in
68
+ // ./cliDispatch so it can be tested without running this entry point.
69
+ async function dispatchCatalog(words, argv, client, defaultChannel) {
70
+ const match = (0, cliDispatch_1.resolveTool)(words);
71
+ if (!match) {
72
+ fail(`Unknown command: ${words.join(' ') || '(none)'}\n\n${USAGE}`);
73
+ }
74
+ const { tool, used } = match;
75
+ const args = (0, cliDispatch_1.argsFromArgv)(tool, words.slice(used), argv);
76
+ return print(await client.call(tool.toRequest((0, tools_1.withDefaultChannel)(tool, args, defaultChannel))));
77
+ }
86
78
  const SKIN = (0, client_1.detectSkin)();
87
- const BIN = SKIN === 'wa' ? 'wa' : 'rcs';
79
+ const BRAND = (0, client_1.detectBrand)();
80
+ /** The bin name as the user typed it; the surveys packages ship their own. */
81
+ const BIN = BRAND === 'wa-surveys' || BRAND === 'rcs-surveys' ? BRAND : SKIN === 'wa' ? 'wa' : 'rcs';
82
+ /** Env var prefix: WA_* or RCS_*, never the bin name (wa-surveys → WA_*). */
83
+ const ENV_PREFIX = SKIN === 'wa' ? 'WA' : 'RCS';
88
84
  /** Show the channel this skin actually defaults to in its own help text. */
89
85
  const CHANNEL_EG = SKIN === 'wa' ? 'WHATSAPP' : 'RCS';
90
- const USAGE = `${BIN} — ${(0, client_1.skinLabels)(SKIN).product} agent CLI v${(0, client_1.readVersion)()}
86
+ const USAGE = `${BIN} — ${(0, client_1.skinLabels)(BRAND).product} agent CLI v${(0, client_1.readVersion)()}
91
87
 
92
88
  Usage:
93
89
  ${BIN} capabilities
@@ -113,9 +109,18 @@ Surveys:
113
109
  ${BIN} survey sends <id>
114
110
  ${BIN} survey results <id>
115
111
 
116
- Config: --url/${BIN.toUpperCase()}_GATEWAY_URL, --key/${BIN.toUpperCase()}_AGENT_KEY`;
112
+ Everything else, straight from the tool catalog (scope shown after each):
113
+ ${(0, cliDispatch_1.catalogUsage)(BIN)}
114
+
115
+ Any catalog tool name also works as typed (${BIN} list_conversations, or
116
+ ${BIN} list-conversations). Structured arguments come from --file <doc.json>:
117
+ an object merges at the top level, a bare array fills the tool's list argument.
118
+ Repeat a flag or comma-separate it for lists (--event a --event b).
119
+
120
+ Config: --key/${ENV_PREFIX}_AGENT_KEY (required),
121
+ --url/${ENV_PREFIX}_GATEWAY_URL (optional; defaults to the hosted gateway)`;
117
122
  async function run() {
118
- const argv = parseArgs(process.argv.slice(2));
123
+ const argv = (0, cliDispatch_1.parseArgs)(process.argv.slice(2));
119
124
  const cmd = argv._[0];
120
125
  if (!cmd || cmd === 'help' || argv.flags.help) {
121
126
  process.stdout.write(`${USAGE}\n`);
@@ -340,12 +345,12 @@ async function run() {
340
345
  path: `/surveys/${enc(id)}/results`
341
346
  }));
342
347
  default:
343
- fail(`Unknown survey subcommand: ${sub ?? '(none)'}\n\n${USAGE}`);
348
+ // `survey render|clone|delete` and anything the catalog grows later.
349
+ return dispatchCatalog(argv._, argv, client, config.defaultChannel);
344
350
  }
345
- return;
346
351
  }
347
352
  default:
348
- fail(`Unknown command: ${cmd}\n\n${USAGE}`);
353
+ return dispatchCatalog(argv._, argv, client, config.defaultChannel);
349
354
  }
350
355
  }
351
356
  function str(v) {
@@ -363,6 +368,10 @@ function fail(message) {
363
368
  process.exit(2);
364
369
  }
365
370
  run().catch((err) => {
371
+ if (err instanceof cliDispatch_1.UsageError) {
372
+ // Bad arguments, not a failed call — same exit 2 the curated commands use.
373
+ fail(err.message);
374
+ }
366
375
  if (err instanceof client_1.GatewayError) {
367
376
  process.stderr.write(`Gateway ${err.status}: ${err.message}\n`);
368
377
  if (err.details)
@@ -0,0 +1,65 @@
1
+ import { type ToolDef } from './tools';
2
+ export interface ParsedArgs {
3
+ _: string[];
4
+ flags: Record<string, string | boolean>;
5
+ multi: Record<string, string[]>;
6
+ /**
7
+ * EVERY flag's values in order, including the ones `multi` claims and the
8
+ * ones `flags` collapses to their last occurrence. Catalog dispatch needs
9
+ * this: an array-typed argument (`--event a --event b`) cannot be expressed
10
+ * by either of the other two, and hardcoding another name into `multi` would
11
+ * put us back to editing this file whenever the gateway grows an argument.
12
+ */
13
+ repeats: Record<string, string[]>;
14
+ }
15
+ /**
16
+ * A caller error — bad or missing arguments, unknown command.
17
+ *
18
+ * Thrown rather than exiting so these functions stay testable; `cli.ts` turns
19
+ * it into the same stderr + exit 2 every usage error has always produced.
20
+ */
21
+ export declare class UsageError extends Error {
22
+ }
23
+ /** Minimal argv parser: --flag value, --bool, and repeatable --to/--var. */
24
+ export declare function parseArgs(argv: string[]): ParsedArgs;
25
+ /**
26
+ * Catalog tools that already have a curated command, listed only so generated
27
+ * help does not print a second, uglier spelling of a command that has a good
28
+ * one. Being in this set does not disable catalog dispatch for the tool.
29
+ */
30
+ export declare const CURATED: Set<string>;
31
+ /**
32
+ * Friendlier spellings for generically-dispatched tools: `wa audience get`
33
+ * rather than `wa get_audience`. The catalog name always works too, in snake or
34
+ * kebab case, so a tool added tomorrow is callable before anyone writes it an
35
+ * alias here.
36
+ */
37
+ export declare const ALIASES: Record<string, string>;
38
+ /** The `:param` names in a tool's route, in the order the route names them. */
39
+ export declare function pathParams(tool: ToolDef): string[];
40
+ /** The friendly spelling of a tool, for help text and error messages. */
41
+ export declare function cliName(tool: ToolDef): string;
42
+ /**
43
+ * Resolve argv's leading words to a catalog tool. Two words are tried first so
44
+ * `audience get` wins over a tool literally named `audience`; the number of
45
+ * words consumed comes back so the caller knows where positionals start.
46
+ */
47
+ export declare function resolveTool(words: string[]): {
48
+ tool: ToolDef;
49
+ used: number;
50
+ } | undefined;
51
+ /** Coerce raw flag strings to the JSON-Schema type the tool declares. */
52
+ export declare function coerce(schema: any, values: string[]): unknown;
53
+ /**
54
+ * Build a tool's arguments from argv: positionals fill the route's `:params`,
55
+ * flags fill everything else, and `--file` supplies the structured ones — an
56
+ * object merges at the top level, a bare array fills the tool's single
57
+ * array-typed argument, so `--file contacts.json` works whether the file holds
58
+ * `[…]` or `{"contacts":[…]}`.
59
+ */
60
+ export declare function argsFromArgv(tool: ToolDef, positionals: string[], argv: ParsedArgs, readFile?: (path: string) => string): Record<string, any>;
61
+ /** One tool's usage line, derived from its schema rather than written by hand. */
62
+ export declare function toolUsage(tool: ToolDef, bin?: string): string;
63
+ /** The generated half of the help: every catalog tool without a curated command. */
64
+ export declare function catalogUsage(bin?: string): string;
65
+ //# sourceMappingURL=cliDispatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cliDispatch.d.ts","sourceRoot":"","sources":["../src/cliDispatch.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAmB,KAAK,OAAO,EAAE,MAAM,SAAS,CAAC;AAExD,MAAM,WAAW,UAAU;IACzB,CAAC,EAAE,MAAM,EAAE,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC;IACxC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAChC;;;;;;OAMG;IACH,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACnC;AAED;;;;;GAKG;AACH,qBAAa,UAAW,SAAQ,KAAK;CAAG;AAExC,4EAA4E;AAC5E,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CAyBpD;AAED;;;;GAIG;AACH,eAAO,MAAM,OAAO,aAyBlB,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAwB1C,CAAC;AAEF,+EAA+E;AAC/E,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,EAAE,CAElD;AAED,yEAAyE;AACzE,wBAAgB,OAAO,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAG7C;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,EAAE,GACd;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAa7C;AAED,yEAAyE;AACzE,wBAAgB,MAAM,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CA4B7D;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,OAAO,EACb,WAAW,EAAE,MAAM,EAAE,EACrB,IAAI,EAAE,UAAU,EAChB,QAAQ,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAuC,GAClE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAiDrB;AAED,kFAAkF;AAClF,wBAAgB,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,SAAQ,GAAG,MAAM,CAc5D;AAED,oFAAoF;AACpF,wBAAgB,YAAY,CAAC,GAAG,SAAQ,GAAG,MAAM,CAIhD"}
@@ -0,0 +1,260 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ALIASES = exports.CURATED = exports.UsageError = void 0;
4
+ exports.parseArgs = parseArgs;
5
+ exports.pathParams = pathParams;
6
+ exports.cliName = cliName;
7
+ exports.resolveTool = resolveTool;
8
+ exports.coerce = coerce;
9
+ exports.argsFromArgv = argsFromArgv;
10
+ exports.toolUsage = toolUsage;
11
+ exports.catalogUsage = catalogUsage;
12
+ /**
13
+ * Argument parsing and catalog dispatch for the `rcs` / `wa` CLI.
14
+ *
15
+ * Split out of `cli.ts` because that file is an entry point: it calls `run()`
16
+ * at module scope, so importing it to test anything would run the CLI. These
17
+ * are the pure parts — argv in, tool arguments out — and they are where the
18
+ * behaviour worth testing lives.
19
+ *
20
+ * WHY CATALOG DISPATCH EXISTS: `tools.ts` already declares every gateway
21
+ * capability (route, schema, scope) and a CI test fails when a route has no
22
+ * entry. The CLI used to hand-write its own routes anyway, so it drifted to
23
+ * ~20 of the catalog's 48 tools while the MCP server had all of them. Now the
24
+ * curated commands stay hand-written — they earn it, folding several flags into
25
+ * one endpoint — and everything else is reached through the catalog, so a new
26
+ * capability is callable from the CLI the moment it has a catalog entry.
27
+ */
28
+ const node_fs_1 = require("node:fs");
29
+ const tools_1 = require("./tools");
30
+ /**
31
+ * A caller error — bad or missing arguments, unknown command.
32
+ *
33
+ * Thrown rather than exiting so these functions stay testable; `cli.ts` turns
34
+ * it into the same stderr + exit 2 every usage error has always produced.
35
+ */
36
+ class UsageError extends Error {
37
+ }
38
+ exports.UsageError = UsageError;
39
+ /** Minimal argv parser: --flag value, --bool, and repeatable --to/--var. */
40
+ function parseArgs(argv) {
41
+ const out = {
42
+ _: [],
43
+ flags: {},
44
+ multi: { to: [], var: [], audience: [] },
45
+ repeats: {}
46
+ };
47
+ for (let i = 0; i < argv.length; i++) {
48
+ const a = argv[i];
49
+ if (a.startsWith('--')) {
50
+ const key = a.slice(2);
51
+ const next = argv[i + 1];
52
+ const takesValue = next !== undefined && !next.startsWith('--');
53
+ const value = takesValue ? argv[++i] : true;
54
+ if (typeof value === 'string')
55
+ (out.repeats[key] ??= []).push(value);
56
+ if (key === 'to' || key === 'var' || key === 'audience') {
57
+ if (typeof value === 'string')
58
+ out.multi[key].push(value);
59
+ }
60
+ else {
61
+ out.flags[key] = value;
62
+ }
63
+ }
64
+ else {
65
+ out._.push(a);
66
+ }
67
+ }
68
+ return out;
69
+ }
70
+ /**
71
+ * Catalog tools that already have a curated command, listed only so generated
72
+ * help does not print a second, uglier spelling of a command that has a good
73
+ * one. Being in this set does not disable catalog dispatch for the tool.
74
+ */
75
+ exports.CURATED = new Set([
76
+ 'capabilities',
77
+ 'search_templates',
78
+ 'get_template',
79
+ 'render_template',
80
+ 'create_template',
81
+ 'clone_template',
82
+ 'submit_template',
83
+ 'send_message',
84
+ 'upload_media',
85
+ 'get_knowledge',
86
+ 'set_knowledge',
87
+ 'delete_knowledge',
88
+ 'enable_knowledge',
89
+ 'disable_knowledge',
90
+ 'get_usage',
91
+ 'search_surveys',
92
+ 'get_survey',
93
+ 'create_survey',
94
+ 'update_survey',
95
+ 'submit_survey',
96
+ 'send_survey',
97
+ 'send_survey_test',
98
+ 'list_survey_sends',
99
+ 'get_results'
100
+ ]);
101
+ /**
102
+ * Friendlier spellings for generically-dispatched tools: `wa audience get`
103
+ * rather than `wa get_audience`. The catalog name always works too, in snake or
104
+ * kebab case, so a tool added tomorrow is callable before anyone writes it an
105
+ * alias here.
106
+ */
107
+ exports.ALIASES = {
108
+ audiences: 'search_audiences',
109
+ 'audience get': 'get_audience',
110
+ 'audience contacts': 'list_audience_contacts',
111
+ 'audience create': 'create_audience',
112
+ 'audience import': 'import_contacts',
113
+ 'audience delete': 'delete_audience',
114
+ contacts: 'list_contacts',
115
+ 'contact get': 'get_contact',
116
+ 'contact update': 'update_contact',
117
+ conversations: 'list_conversations',
118
+ 'conversation get': 'get_conversation',
119
+ 'conversation assign': 'assign_conversation',
120
+ webhooks: 'list_webhooks',
121
+ 'webhook create': 'create_webhook',
122
+ 'webhook update': 'update_webhook',
123
+ 'webhook delete': 'delete_webhook',
124
+ 'webhook test': 'test_webhook',
125
+ 'template update': 'update_template',
126
+ 'template duplicate': 'duplicate_template',
127
+ 'template delete': 'delete_template',
128
+ 'survey render': 'render_survey',
129
+ 'survey clone': 'clone_survey',
130
+ 'survey delete': 'delete_survey'
131
+ };
132
+ /** The `:param` names in a tool's route, in the order the route names them. */
133
+ function pathParams(tool) {
134
+ return (tool.path.match(/:([A-Za-z0-9_]+)/g) ?? []).map((s) => s.slice(1));
135
+ }
136
+ /** The friendly spelling of a tool, for help text and error messages. */
137
+ function cliName(tool) {
138
+ const alias = Object.entries(exports.ALIASES).find(([, name]) => name === tool.name);
139
+ return alias ? alias[0] : tool.name;
140
+ }
141
+ /**
142
+ * Resolve argv's leading words to a catalog tool. Two words are tried first so
143
+ * `audience get` wins over a tool literally named `audience`; the number of
144
+ * words consumed comes back so the caller knows where positionals start.
145
+ */
146
+ function resolveTool(words) {
147
+ const two = words.slice(0, 2).join(' ');
148
+ if (words.length >= 2 && exports.ALIASES[two]) {
149
+ const tool = (0, tools_1.findTool)(exports.ALIASES[two]);
150
+ if (tool)
151
+ return { tool, used: 2 };
152
+ }
153
+ const one = words[0];
154
+ if (!one)
155
+ return undefined;
156
+ const direct = (exports.ALIASES[one] ? (0, tools_1.findTool)(exports.ALIASES[one]) : undefined) ??
157
+ (0, tools_1.findTool)(one) ??
158
+ (0, tools_1.findTool)(one.replace(/-/g, '_'));
159
+ return direct ? { tool: direct, used: 1 } : undefined;
160
+ }
161
+ /** Coerce raw flag strings to the JSON-Schema type the tool declares. */
162
+ function coerce(schema, values) {
163
+ const raw = schema?.type;
164
+ const type = Array.isArray(raw) ? raw.find((t) => t !== 'null') : raw;
165
+ if (type === 'array') {
166
+ // Repeat the flag, or comma-separate it — both read naturally in a shell.
167
+ const items = values.length > 1 ? values : values[0].split(',');
168
+ const itemType = schema.items?.type;
169
+ return items.map((v) => itemType === 'number' || itemType === 'integer'
170
+ ? Number(v)
171
+ : itemType === 'boolean'
172
+ ? v !== 'false'
173
+ : v);
174
+ }
175
+ const last = values[values.length - 1];
176
+ if (type === 'number' || type === 'integer')
177
+ return Number(last);
178
+ if (type === 'boolean')
179
+ return last !== 'false';
180
+ if (type === 'object') {
181
+ try {
182
+ return JSON.parse(last);
183
+ }
184
+ catch {
185
+ throw new UsageError('an object argument must be JSON on the command line, or come from --file');
186
+ }
187
+ }
188
+ return last;
189
+ }
190
+ /**
191
+ * Build a tool's arguments from argv: positionals fill the route's `:params`,
192
+ * flags fill everything else, and `--file` supplies the structured ones — an
193
+ * object merges at the top level, a bare array fills the tool's single
194
+ * array-typed argument, so `--file contacts.json` works whether the file holds
195
+ * `[…]` or `{"contacts":[…]}`.
196
+ */
197
+ function argsFromArgv(tool, positionals, argv, readFile = (p) => (0, node_fs_1.readFileSync)(p, 'utf8')) {
198
+ const props = (tool.inputSchema.properties ?? {});
199
+ const args = {};
200
+ const file = typeof argv.flags.file === 'string' ? argv.flags.file : undefined;
201
+ if (file) {
202
+ const doc = JSON.parse(readFile(file));
203
+ if (Array.isArray(doc)) {
204
+ const arrayProp = Object.keys(props).find((k) => props[k]?.type === 'array');
205
+ if (!arrayProp) {
206
+ throw new UsageError(`${cliName(tool)} takes no list argument — --file must hold a JSON object`);
207
+ }
208
+ args[arrayProp] = doc;
209
+ }
210
+ else if (doc && typeof doc === 'object') {
211
+ Object.assign(args, doc);
212
+ }
213
+ else {
214
+ throw new UsageError('--file must hold a JSON object or array');
215
+ }
216
+ }
217
+ for (const [key, schema] of Object.entries(props)) {
218
+ if (key === 'file')
219
+ continue;
220
+ const values = argv.repeats[key];
221
+ if (values?.length) {
222
+ args[key] = coerce(schema, values);
223
+ }
224
+ else if (schema?.type === 'boolean' && argv.flags[key] === true) {
225
+ args[key] = true;
226
+ }
227
+ }
228
+ pathParams(tool).forEach((param, i) => {
229
+ if (args[param] === undefined && positionals[i] !== undefined) {
230
+ args[param] = positionals[i];
231
+ }
232
+ });
233
+ const missing = (tool.inputSchema.required ?? []).filter((k) => args[k] === undefined);
234
+ if (missing.length) {
235
+ throw new UsageError(`${cliName(tool)} is missing: ${missing.join(', ')}\n\n${toolUsage(tool)}`);
236
+ }
237
+ return args;
238
+ }
239
+ /** One tool's usage line, derived from its schema rather than written by hand. */
240
+ function toolUsage(tool, bin = 'rcs') {
241
+ const props = (tool.inputSchema.properties ?? {});
242
+ const required = new Set(tool.inputSchema.required ?? []);
243
+ const params = pathParams(tool);
244
+ const flags = Object.keys(props)
245
+ .filter((k) => !params.includes(k))
246
+ .map((k) => {
247
+ // A nullable argument declares `["string","null"]`; read it as string|null.
248
+ const raw = props[k]?.type;
249
+ const type = Array.isArray(raw) ? raw.join('|') : (raw ?? 'value');
250
+ const one = type === 'boolean' ? `--${k}` : `--${k} <${type}>`;
251
+ return required.has(k) ? one : `[${one}]`;
252
+ });
253
+ return ` ${bin} ${[cliName(tool), ...params.map((p) => `<${p}>`), ...flags].join(' ')}`;
254
+ }
255
+ /** The generated half of the help: every catalog tool without a curated command. */
256
+ function catalogUsage(bin = 'rcs') {
257
+ return tools_1.TOOLS.filter((t) => !exports.CURATED.has(t.name))
258
+ .map((t) => `${toolUsage(t, bin)} # ${t.scope}`)
259
+ .join('\n');
260
+ }
package/dist/client.d.ts CHANGED
@@ -41,6 +41,14 @@ export interface GatewayConfig {
41
41
  * argv, a bundler-rewritten path, or a direct import all fall through to 'rcs'.
42
42
  */
43
43
  export declare function detectSkin(argv?: readonly string[]): 'wa' | 'rcs';
44
+ /**
45
+ * The product a bin is branded as. The surveys packages (@wa-surveys/…,
46
+ * @rcs-surveys/…) are aliases like @wa-campaigns/…: same gateway, same catalog.
47
+ * The brand only changes the words the host and the model read; the channel
48
+ * default and env prefix still come from {@link detectSkin}.
49
+ */
50
+ export type Brand = 'rcs-campaigns' | 'wa-campaigns' | 'rcs-surveys' | 'wa-surveys';
51
+ export declare function detectBrand(argv?: readonly string[]): Brand;
44
52
  /**
45
53
  * The version of the package the user actually installed, read at runtime rather
46
54
  * than restated in a constant. A hardcoded copy silently goes stale the first
@@ -61,12 +69,22 @@ export declare function detectSkin(argv?: readonly string[]): 'wa' | 'rcs';
61
69
  * and `dist/mcp.js` alike.
62
70
  */
63
71
  export declare function readVersion(argv?: readonly string[]): string;
64
- /** Human-readable product + channel names for the running skin. */
65
- export declare function skinLabels(skin?: 'wa' | 'rcs'): {
72
+ /**
73
+ * Human-readable product + channel names for the running bin. Accepts a skin
74
+ * ('wa' | 'rcs', meaning the campaigns brand) or a full brand.
75
+ */
76
+ export declare function skinLabels(skinOrBrand?: 'wa' | 'rcs' | Brand): {
66
77
  product: string;
67
78
  channel: string;
68
- slug: string;
79
+ slug: Brand;
69
80
  };
81
+ /**
82
+ * The hosted production gateway. Both skins (wa and rcs) talk to this one
83
+ * deployment, so a customer only has to supply an agent key. Set
84
+ * WA_GATEWAY_URL / RCS_GATEWAY_URL (or --url) to point at dev or a local
85
+ * gateway instead.
86
+ */
87
+ export declare const DEFAULT_GATEWAY_URL = "https://rcs-templates-api-dot-msg2ai-server.ue.r.appspot.com";
70
88
  /**
71
89
  * Normalize a channel from configuration or a tool argument. Accepts the channel
72
90
  * enum itself (any case) AND a brand id — canonical or legacy — resolving it to
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,4EAA4E;AAC5E,MAAM,MAAM,OAAO,GAAG,KAAK,GAAG,UAAU,CAAC;AAEzC,MAAM,WAAW,aAAa;IAC5B,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAC;IAChB,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,IAAI,GAAE,SAAS,MAAM,EAAiB,GAAG,IAAI,GAAG,KAAK,CAG/E;AAmCD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,WAAW,CAAC,IAAI,GAAE,SAAS,MAAM,EAAiB,GAAG,MAAM,CAO1E;AAED,mEAAmE;AACnE,wBAAgB,UAAU,CAAC,IAAI,GAAE,IAAI,GAAG,KAAoB;;;;EAI3D;AA6BD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,SAAS,CAMpE;AAOD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;IACpD,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAAC;IAC9D,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,8EAA8E;AAC9E,qBAAa,YAAa,SAAQ,KAAK;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;gBACN,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO;CAM/D;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,aAAa,CA4B7E;AAED,qBAAa,aAAa;IACZ,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,aAAa;IAElD,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,aAAa;IAI/D,+EAA+E;IACzE,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC;CAmCvD"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,4EAA4E;AAC5E,MAAM,MAAM,OAAO,GAAG,KAAK,GAAG,UAAU,CAAC;AAEzC,MAAM,WAAW,aAAa;IAC5B,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAC;IAChB,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,IAAI,GAAE,SAAS,MAAM,EAAiB,GAAG,IAAI,GAAG,KAAK,CAE/E;AAED;;;;;GAKG;AACH,MAAM,MAAM,KAAK,GAAG,eAAe,GAAG,cAAc,GAAG,aAAa,GAAG,YAAY,CAAC;AAEpF,wBAAgB,WAAW,CAAC,IAAI,GAAE,SAAS,MAAM,EAAiB,GAAG,KAAK,CAczE;AA8CD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,WAAW,CAAC,IAAI,GAAE,SAAS,MAAM,EAAiB,GAAG,MAAM,CAO1E;AASD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,WAAW,GAAE,IAAI,GAAG,KAAK,GAAG,KAAqB;aAX/B,MAAM;aAAW,MAAM;UAAQ,KAAK;EAehF;AAED;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,iEACgC,CAAC;AA6BjE;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,SAAS,CAMpE;AAOD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;IACpD,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAAC;IAC9D,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,8EAA8E;AAC9E,qBAAa,YAAa,SAAQ,KAAK;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;gBACN,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO;CAM/D;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,aAAa,CA4B7E;AAED,qBAAa,aAAa;IACZ,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,aAAa;IAElD,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,aAAa;IAI/D,+EAA+E;IACzE,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC;CAmCvD"}
package/dist/client.js CHANGED
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.GatewayClient = exports.GatewayError = void 0;
3
+ exports.GatewayClient = exports.GatewayError = exports.DEFAULT_GATEWAY_URL = void 0;
4
4
  exports.detectSkin = detectSkin;
5
+ exports.detectBrand = detectBrand;
5
6
  exports.readVersion = readVersion;
6
7
  exports.skinLabels = skinLabels;
7
8
  exports.normalizeChannel = normalizeChannel;
@@ -18,8 +19,22 @@ const node_path_1 = require("node:path");
18
19
  * argv, a bundler-rewritten path, or a direct import all fall through to 'rcs'.
19
20
  */
20
21
  function detectSkin(argv = process.argv) {
21
- const name = invokedBinName(argv);
22
- return name === 'wa' || name === 'wa-mcp' ? 'wa' : 'rcs';
22
+ return detectBrand(argv).startsWith('wa-') ? 'wa' : 'rcs';
23
+ }
24
+ function detectBrand(argv = process.argv) {
25
+ switch (invokedBinName(argv)) {
26
+ case 'wa':
27
+ case 'wa-mcp':
28
+ return 'wa-campaigns';
29
+ case 'wa-surveys':
30
+ case 'wa-surveys-mcp':
31
+ return 'wa-surveys';
32
+ case 'rcs-surveys':
33
+ case 'rcs-surveys-mcp':
34
+ return 'rcs-surveys';
35
+ default:
36
+ return 'rcs-campaigns';
37
+ }
23
38
  }
24
39
  /**
25
40
  * The bin basename this process was launched as — "wa-mcp", "mcp", "cli" — or
@@ -42,7 +57,18 @@ function invokedBinName(argv) {
42
57
  * — hence "mcp" and "cli" rather than "rcs-mcp" and "rcs". Anything not in this
43
58
  * set is not one of ours, and is never trusted to locate a package.
44
59
  */
45
- const OWN_BINS = new Set(['wa', 'wa-mcp', 'rcs', 'rcs-mcp', 'mcp', 'cli']);
60
+ const OWN_BINS = new Set([
61
+ 'wa',
62
+ 'wa-mcp',
63
+ 'rcs',
64
+ 'rcs-mcp',
65
+ 'mcp',
66
+ 'cli',
67
+ 'wa-surveys',
68
+ 'wa-surveys-mcp',
69
+ 'rcs-surveys',
70
+ 'rcs-surveys-mcp'
71
+ ]);
46
72
  /** The `version` of the package.json in `dir`, or undefined if unreadable. */
47
73
  function packageVersionAt(dir) {
48
74
  try {
@@ -81,12 +107,27 @@ function readVersion(argv = process.argv) {
81
107
  }
82
108
  return packageVersionAt((0, node_path_1.join)(__dirname, '..')) ?? '0.0.0';
83
109
  }
84
- /** Human-readable product + channel names for the running skin. */
85
- function skinLabels(skin = detectSkin()) {
86
- return skin === 'wa'
87
- ? { product: 'WhatsApp Campaigns', channel: 'WhatsApp', slug: 'wa-campaigns' }
88
- : { product: 'RCS Campaigns', channel: 'RCS', slug: 'rcs-campaigns' };
110
+ const BRAND_LABELS = {
111
+ 'wa-campaigns': { product: 'WhatsApp Campaigns', channel: 'WhatsApp', slug: 'wa-campaigns' },
112
+ 'rcs-campaigns': { product: 'RCS Campaigns', channel: 'RCS', slug: 'rcs-campaigns' },
113
+ 'wa-surveys': { product: 'WhatsApp Surveys', channel: 'WhatsApp', slug: 'wa-surveys' },
114
+ 'rcs-surveys': { product: 'RCS Surveys', channel: 'RCS', slug: 'rcs-surveys' }
115
+ };
116
+ /**
117
+ * Human-readable product + channel names for the running bin. Accepts a skin
118
+ * ('wa' | 'rcs', meaning the campaigns brand) or a full brand.
119
+ */
120
+ function skinLabels(skinOrBrand = detectBrand()) {
121
+ const brand = skinOrBrand === 'wa' ? 'wa-campaigns' : skinOrBrand === 'rcs' ? 'rcs-campaigns' : skinOrBrand;
122
+ return { ...BRAND_LABELS[brand] };
89
123
  }
124
+ /**
125
+ * The hosted production gateway. Both skins (wa and rcs) talk to this one
126
+ * deployment, so a customer only has to supply an agent key. Set
127
+ * WA_GATEWAY_URL / RCS_GATEWAY_URL (or --url) to point at dev or a local
128
+ * gateway instead.
129
+ */
130
+ exports.DEFAULT_GATEWAY_URL = 'https://rcs-templates-api-dot-msg2ai-server.ue.r.appspot.com';
90
131
  /** First environment variable that is set and non-empty, under either prefix. */
91
132
  function fromEnv(...names) {
92
133
  for (const name of names) {
@@ -159,7 +200,7 @@ function resolveConfig(partial) {
159
200
  const [keyFirst, keySecond] = skin === 'wa'
160
201
  ? ['WA_AGENT_KEY', 'RCS_AGENT_KEY']
161
202
  : ['RCS_AGENT_KEY', 'WA_AGENT_KEY'];
162
- const baseUrl = partial?.baseUrl ?? fromEnv(urlFirst, urlSecond) ?? 'http://localhost:3002';
203
+ const baseUrl = partial?.baseUrl ?? fromEnv(urlFirst, urlSecond) ?? exports.DEFAULT_GATEWAY_URL;
163
204
  const apiKey = partial?.apiKey ?? fromEnv(keyFirst, keySecond) ?? '';
164
205
  if (!apiKey) {
165
206
  throw new Error(`No agent key configured. Set ${keyFirst} or pass apiKey explicitly.`);
package/dist/mcp.js CHANGED
@@ -11,13 +11,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
11
11
  * runtime dependencies — nothing to install, nothing to drift, and it slots into
12
12
  * the workspace without touching the lockfile's external graph.
13
13
  *
14
- * Credentials: the agent key + gateway URL come from the environment
15
- * (RCS_AGENT_KEY / RCS_GATEWAY_URL). This process holds the key only to forward
14
+ * Credentials: the agent key comes from the environment (RCS_AGENT_KEY); the
15
+ * gateway URL defaults to the hosted production gateway and can be overridden
16
+ * with RCS_GATEWAY_URL. This process holds the key only to forward
16
17
  * it as a Bearer token to the gateway — all business logic stays server-side.
17
18
  *
18
19
  * Wire it up (e.g. Claude Code):
19
20
  * claude mcp add rcs-campaigns -- npx -y -p @rcs-campaigns/agent-toolkit rcs-mcp
20
- * (with RCS_AGENT_KEY + RCS_GATEWAY_URL set in the environment)
21
+ * (with RCS_AGENT_KEY set in the environment)
21
22
  */
22
23
  const node_readline_1 = require("node:readline");
23
24
  const client_1 = require("./client");
@@ -31,7 +32,7 @@ const PROTOCOL_VERSION = '2024-11-05';
31
32
  * Same catalog, same gateway — only the words the model reads differ.
32
33
  */
33
34
  const SKIN = (0, client_1.detectSkin)();
34
- const { product: PRODUCT, channel: CHANNEL, slug: SLUG } = (0, client_1.skinLabels)(SKIN);
35
+ const { product: PRODUCT, channel: CHANNEL, slug: SLUG } = (0, client_1.skinLabels)((0, client_1.detectBrand)());
35
36
  const SERVER_INFO = { name: SLUG, version: (0, client_1.readVersion)() };
36
37
  /**
37
38
  * Surfaced to MCP hosts as the initialize result's `instructions` string — hosts
package/dist/tools.js CHANGED
@@ -102,7 +102,7 @@ exports.TOOLS = [
102
102
  {
103
103
  name: 'search_templates',
104
104
  description: 'Search the published RCS template marketplace. Returns normalized listings (id, name, rating, cloneCount, categoryKeys).',
105
- scope: 'templates:read',
105
+ scope: 'campaigns:read',
106
106
  method: 'GET',
107
107
  path: '/templates',
108
108
  inputSchema: {
@@ -139,7 +139,7 @@ exports.TOOLS = [
139
139
  {
140
140
  name: 'get_template',
141
141
  description: 'Fetch one template by id (a published marketplace listing or one your org owns).',
142
- scope: 'templates:read',
142
+ scope: 'campaigns:read',
143
143
  method: 'GET',
144
144
  path: '/templates/:id',
145
145
  inputSchema: {
@@ -156,7 +156,7 @@ exports.TOOLS = [
156
156
  {
157
157
  name: 'render_template',
158
158
  description: 'Validate a template and return the rendered RCS payload + variable-bound SMS/MMS fallback, without sending. Use to inspect before cloning/sending.',
159
- scope: 'templates:read',
159
+ scope: 'campaigns:read',
160
160
  method: 'POST',
161
161
  path: '/templates/:id/render',
162
162
  inputSchema: {
@@ -181,7 +181,7 @@ exports.TOOLS = [
181
181
  {
182
182
  name: 'create_template',
183
183
  description: 'Create a template draft in your org. Compose a TemplateContent (text/card/carousel) with a REQUIRED non-empty smsFallback; the shared RCS validator gates it. Submit it (submit_template) and await provider approval before send_message for rich RCS; an unsubmitted template delivers as SMS fallback.',
184
- scope: 'templates:write',
184
+ scope: 'campaigns:write',
185
185
  method: 'POST',
186
186
  path: '/templates',
187
187
  successStatus: 201,
@@ -220,7 +220,7 @@ exports.TOOLS = [
220
220
  {
221
221
  name: 'update_template',
222
222
  description: 'Partially update a template your org owns: name, description, category, tags, knowledgeDoc, and content (re-validated by the shared RCS validator — a non-empty smsFallback is still required). Only the fields you pass change. Editing the CONTENT of an already-approved template invalidates that approval: submit_template it again and await approval, or it delivers as SMS fallback until then. Use get_template first to edit incrementally rather than overwriting blind.',
223
- scope: 'templates:write',
223
+ scope: 'campaigns:write',
224
224
  method: 'PUT',
225
225
  path: '/templates/:id',
226
226
  inputSchema: {
@@ -265,7 +265,7 @@ exports.TOOLS = [
265
265
  {
266
266
  name: 'duplicate_template',
267
267
  description: 'Duplicate a template your org OWNS into a fresh private draft (e.g. to iterate a variant without touching the original). The copy keeps the content, channel, category, tags and knowledge base, but starts unsubmitted — it has no provider approval of its own until you submit_template it. For copying someone else\'s PUBLISHED marketplace listing, use clone_template instead.',
268
- scope: 'templates:write',
268
+ scope: 'campaigns:write',
269
269
  method: 'POST',
270
270
  path: '/templates/:id/duplicate',
271
271
  successStatus: 201,
@@ -283,7 +283,7 @@ exports.TOOLS = [
283
283
  {
284
284
  name: 'delete_template',
285
285
  description: 'Delete a template your org owns. IRREVERSIBLE, and it also removes the template from the provider (Twilio), so an approved template loses its approval and can no longer be sent. Broadcasts that already went out are unaffected. Prefer this only for drafts and abandoned iterations; ask the user before deleting anything they have submitted or sent.',
286
- scope: 'templates:write',
286
+ scope: 'campaigns:write',
287
287
  method: 'DELETE',
288
288
  path: '/templates/:id',
289
289
  inputSchema: {
@@ -318,7 +318,7 @@ exports.TOOLS = [
318
318
  {
319
319
  name: 'submit_template',
320
320
  description: 'Submit an org-owned template to the provider (Twilio) for approval. Returns the contentSid + approval status.',
321
- scope: 'templates:submit',
321
+ scope: 'campaigns:submit',
322
322
  method: 'POST',
323
323
  path: '/templates/:id/submit',
324
324
  inputSchema: {
@@ -685,7 +685,7 @@ exports.TOOLS = [
685
685
  {
686
686
  name: 'get_knowledge',
687
687
  description: "Read a template's knowledge base: the grounding document text plus whether answering is enabled. Use before set_knowledge to edit a doc incrementally instead of overwriting it blind.",
688
- scope: 'templates:read',
688
+ scope: 'campaigns:read',
689
689
  method: 'GET',
690
690
  path: '/templates/:id/knowledge',
691
691
  inputSchema: {
@@ -702,7 +702,7 @@ exports.TOOLS = [
702
702
  {
703
703
  name: 'set_knowledge',
704
704
  description: 'Set/replace a template\'s knowledge base: the grounding document AI answers are generated from when recipients reply. Include operating instructions in the doc itself — e.g. "Any question not answered by this document must be answered exactly: …". Setting a doc re-enables answering.',
705
- scope: 'templates:write',
705
+ scope: 'campaigns:write',
706
706
  method: 'PUT',
707
707
  path: '/templates/:id/knowledge',
708
708
  inputSchema: {
@@ -726,7 +726,7 @@ exports.TOOLS = [
726
726
  {
727
727
  name: 'delete_knowledge',
728
728
  description: 'Delete a template\'s knowledge base document.',
729
- scope: 'templates:write',
729
+ scope: 'campaigns:write',
730
730
  method: 'DELETE',
731
731
  path: '/templates/:id/knowledge',
732
732
  inputSchema: {
@@ -743,7 +743,7 @@ exports.TOOLS = [
743
743
  {
744
744
  name: 'enable_knowledge',
745
745
  description: 'Enable knowledge answering on a template (the doc is used again).',
746
- scope: 'templates:write',
746
+ scope: 'campaigns:write',
747
747
  method: 'POST',
748
748
  path: '/templates/:id/knowledge/enable',
749
749
  inputSchema: {
@@ -760,7 +760,7 @@ exports.TOOLS = [
760
760
  {
761
761
  name: 'disable_knowledge',
762
762
  description: 'Disable knowledge answering on a template WITHOUT deleting the doc (pause).',
763
- scope: 'templates:write',
763
+ scope: 'campaigns:write',
764
764
  method: 'POST',
765
765
  path: '/templates/:id/knowledge/disable',
766
766
  inputSchema: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rcs-campaigns/agent-toolkit",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "mcpName": "com.rcs-campaigns/campaigns",
5
5
  "description": "Agent skins for the RCS Campaigns gateway: an MCP server (rcs-mcp / wa-mcp) and a CLI (rcs / wa) over one shared client.",
6
6
  "license": "UNLICENSED",