@rcs-campaigns/agent-toolkit 0.1.9 → 0.1.11
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 +9 -8
- package/dist/cli.js +46 -35
- package/dist/cliDispatch.d.ts +65 -0
- package/dist/cliDispatch.d.ts.map +1 -0
- package/dist/cliDispatch.js +271 -0
- package/dist/client.d.ts +21 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +51 -10
- package/dist/mcp.js +6 -6
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +329 -18
- package/package.json +2 -2
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: `
|
|
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.
|
|
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=
|
|
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`
|
|
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
|
|
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
|
-
|
|
33
|
-
|
|
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
|
|
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)(
|
|
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
|
|
@@ -111,11 +107,20 @@ Surveys:
|
|
|
111
107
|
${BIN} survey send <id> [--to +1555... ...] [--audience <audienceId> ...]
|
|
112
108
|
${BIN} survey send-test <id> --to +1555...
|
|
113
109
|
${BIN} survey sends <id>
|
|
114
|
-
${BIN} survey results <id>
|
|
110
|
+
${BIN} survey results <id> [--campaign <campaignId>]
|
|
111
|
+
|
|
112
|
+
Everything else, straight from the tool catalog (scope shown after each):
|
|
113
|
+
${(0, cliDispatch_1.catalogUsage)(BIN)}
|
|
115
114
|
|
|
116
|
-
|
|
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`);
|
|
@@ -334,18 +339,20 @@ async function run() {
|
|
|
334
339
|
path: `/surveys/${enc(id)}/sends`
|
|
335
340
|
}));
|
|
336
341
|
case 'results':
|
|
337
|
-
requireArg(id, 'survey results <id>');
|
|
342
|
+
requireArg(id, 'survey results <id> [--campaign <campaignId>]');
|
|
338
343
|
return print(await client.call({
|
|
339
344
|
method: 'GET',
|
|
340
|
-
path: `/surveys/${enc(id)}/results
|
|
345
|
+
path: `/surveys/${enc(id)}/results`,
|
|
346
|
+
// One send's results (the campaignId `survey send` returns).
|
|
347
|
+
query: { campaignId: str(argv.flags.campaign) }
|
|
341
348
|
}));
|
|
342
349
|
default:
|
|
343
|
-
|
|
350
|
+
// `survey render|clone|delete` and anything the catalog grows later.
|
|
351
|
+
return dispatchCatalog(argv._, argv, client, config.defaultChannel);
|
|
344
352
|
}
|
|
345
|
-
return;
|
|
346
353
|
}
|
|
347
354
|
default:
|
|
348
|
-
|
|
355
|
+
return dispatchCatalog(argv._, argv, client, config.defaultChannel);
|
|
349
356
|
}
|
|
350
357
|
}
|
|
351
358
|
function str(v) {
|
|
@@ -363,6 +370,10 @@ function fail(message) {
|
|
|
363
370
|
process.exit(2);
|
|
364
371
|
}
|
|
365
372
|
run().catch((err) => {
|
|
373
|
+
if (err instanceof cliDispatch_1.UsageError) {
|
|
374
|
+
// Bad arguments, not a failed call — same exit 2 the curated commands use.
|
|
375
|
+
fail(err.message);
|
|
376
|
+
}
|
|
366
377
|
if (err instanceof client_1.GatewayError) {
|
|
367
378
|
process.stderr.write(`Gateway ${err.status}: ${err.message}\n`);
|
|
368
379
|
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,CAmC1C,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,271 @@
|
|
|
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 update': 'update_audience',
|
|
114
|
+
'audience remove': 'remove_audience_contacts',
|
|
115
|
+
'audience delete': 'delete_audience',
|
|
116
|
+
contacts: 'list_contacts',
|
|
117
|
+
'contact get': 'get_contact',
|
|
118
|
+
'contact update': 'update_contact',
|
|
119
|
+
conversations: 'list_conversations',
|
|
120
|
+
'conversation get': 'get_conversation',
|
|
121
|
+
'conversation assign': 'assign_conversation',
|
|
122
|
+
webhooks: 'list_webhooks',
|
|
123
|
+
'webhook create': 'create_webhook',
|
|
124
|
+
'webhook update': 'update_webhook',
|
|
125
|
+
'webhook delete': 'delete_webhook',
|
|
126
|
+
'webhook test': 'test_webhook',
|
|
127
|
+
'template update': 'update_template',
|
|
128
|
+
'template duplicate': 'duplicate_template',
|
|
129
|
+
'template delete': 'delete_template',
|
|
130
|
+
'survey render': 'render_survey',
|
|
131
|
+
'survey clone': 'clone_survey',
|
|
132
|
+
'survey delete': 'delete_survey',
|
|
133
|
+
campaigns: 'list_campaigns',
|
|
134
|
+
analytics: 'get_campaign_analytics',
|
|
135
|
+
groups: 'list_campaign_groups',
|
|
136
|
+
'group get': 'get_campaign_group',
|
|
137
|
+
'group create': 'create_campaign_group',
|
|
138
|
+
'group update': 'update_campaign_group',
|
|
139
|
+
'group delete': 'delete_campaign_group',
|
|
140
|
+
'group add': 'add_campaigns_to_group',
|
|
141
|
+
'group remove': 'remove_campaigns_from_group'
|
|
142
|
+
};
|
|
143
|
+
/** The `:param` names in a tool's route, in the order the route names them. */
|
|
144
|
+
function pathParams(tool) {
|
|
145
|
+
return (tool.path.match(/:([A-Za-z0-9_]+)/g) ?? []).map((s) => s.slice(1));
|
|
146
|
+
}
|
|
147
|
+
/** The friendly spelling of a tool, for help text and error messages. */
|
|
148
|
+
function cliName(tool) {
|
|
149
|
+
const alias = Object.entries(exports.ALIASES).find(([, name]) => name === tool.name);
|
|
150
|
+
return alias ? alias[0] : tool.name;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Resolve argv's leading words to a catalog tool. Two words are tried first so
|
|
154
|
+
* `audience get` wins over a tool literally named `audience`; the number of
|
|
155
|
+
* words consumed comes back so the caller knows where positionals start.
|
|
156
|
+
*/
|
|
157
|
+
function resolveTool(words) {
|
|
158
|
+
const two = words.slice(0, 2).join(' ');
|
|
159
|
+
if (words.length >= 2 && exports.ALIASES[two]) {
|
|
160
|
+
const tool = (0, tools_1.findTool)(exports.ALIASES[two]);
|
|
161
|
+
if (tool)
|
|
162
|
+
return { tool, used: 2 };
|
|
163
|
+
}
|
|
164
|
+
const one = words[0];
|
|
165
|
+
if (!one)
|
|
166
|
+
return undefined;
|
|
167
|
+
const direct = (exports.ALIASES[one] ? (0, tools_1.findTool)(exports.ALIASES[one]) : undefined) ??
|
|
168
|
+
(0, tools_1.findTool)(one) ??
|
|
169
|
+
(0, tools_1.findTool)(one.replace(/-/g, '_'));
|
|
170
|
+
return direct ? { tool: direct, used: 1 } : undefined;
|
|
171
|
+
}
|
|
172
|
+
/** Coerce raw flag strings to the JSON-Schema type the tool declares. */
|
|
173
|
+
function coerce(schema, values) {
|
|
174
|
+
const raw = schema?.type;
|
|
175
|
+
const type = Array.isArray(raw) ? raw.find((t) => t !== 'null') : raw;
|
|
176
|
+
if (type === 'array') {
|
|
177
|
+
// Repeat the flag, or comma-separate it — both read naturally in a shell.
|
|
178
|
+
const items = values.length > 1 ? values : values[0].split(',');
|
|
179
|
+
const itemType = schema.items?.type;
|
|
180
|
+
return items.map((v) => itemType === 'number' || itemType === 'integer'
|
|
181
|
+
? Number(v)
|
|
182
|
+
: itemType === 'boolean'
|
|
183
|
+
? v !== 'false'
|
|
184
|
+
: v);
|
|
185
|
+
}
|
|
186
|
+
const last = values[values.length - 1];
|
|
187
|
+
if (type === 'number' || type === 'integer')
|
|
188
|
+
return Number(last);
|
|
189
|
+
if (type === 'boolean')
|
|
190
|
+
return last !== 'false';
|
|
191
|
+
if (type === 'object') {
|
|
192
|
+
try {
|
|
193
|
+
return JSON.parse(last);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
throw new UsageError('an object argument must be JSON on the command line, or come from --file');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return last;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Build a tool's arguments from argv: positionals fill the route's `:params`,
|
|
203
|
+
* flags fill everything else, and `--file` supplies the structured ones — an
|
|
204
|
+
* object merges at the top level, a bare array fills the tool's single
|
|
205
|
+
* array-typed argument, so `--file contacts.json` works whether the file holds
|
|
206
|
+
* `[…]` or `{"contacts":[…]}`.
|
|
207
|
+
*/
|
|
208
|
+
function argsFromArgv(tool, positionals, argv, readFile = (p) => (0, node_fs_1.readFileSync)(p, 'utf8')) {
|
|
209
|
+
const props = (tool.inputSchema.properties ?? {});
|
|
210
|
+
const args = {};
|
|
211
|
+
const file = typeof argv.flags.file === 'string' ? argv.flags.file : undefined;
|
|
212
|
+
if (file) {
|
|
213
|
+
const doc = JSON.parse(readFile(file));
|
|
214
|
+
if (Array.isArray(doc)) {
|
|
215
|
+
const arrayProp = Object.keys(props).find((k) => props[k]?.type === 'array');
|
|
216
|
+
if (!arrayProp) {
|
|
217
|
+
throw new UsageError(`${cliName(tool)} takes no list argument — --file must hold a JSON object`);
|
|
218
|
+
}
|
|
219
|
+
args[arrayProp] = doc;
|
|
220
|
+
}
|
|
221
|
+
else if (doc && typeof doc === 'object') {
|
|
222
|
+
Object.assign(args, doc);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
throw new UsageError('--file must hold a JSON object or array');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
for (const [key, schema] of Object.entries(props)) {
|
|
229
|
+
if (key === 'file')
|
|
230
|
+
continue;
|
|
231
|
+
const values = argv.repeats[key];
|
|
232
|
+
if (values?.length) {
|
|
233
|
+
args[key] = coerce(schema, values);
|
|
234
|
+
}
|
|
235
|
+
else if (schema?.type === 'boolean' && argv.flags[key] === true) {
|
|
236
|
+
args[key] = true;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
pathParams(tool).forEach((param, i) => {
|
|
240
|
+
if (args[param] === undefined && positionals[i] !== undefined) {
|
|
241
|
+
args[param] = positionals[i];
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
const missing = (tool.inputSchema.required ?? []).filter((k) => args[k] === undefined);
|
|
245
|
+
if (missing.length) {
|
|
246
|
+
throw new UsageError(`${cliName(tool)} is missing: ${missing.join(', ')}\n\n${toolUsage(tool)}`);
|
|
247
|
+
}
|
|
248
|
+
return args;
|
|
249
|
+
}
|
|
250
|
+
/** One tool's usage line, derived from its schema rather than written by hand. */
|
|
251
|
+
function toolUsage(tool, bin = 'rcs') {
|
|
252
|
+
const props = (tool.inputSchema.properties ?? {});
|
|
253
|
+
const required = new Set(tool.inputSchema.required ?? []);
|
|
254
|
+
const params = pathParams(tool);
|
|
255
|
+
const flags = Object.keys(props)
|
|
256
|
+
.filter((k) => !params.includes(k))
|
|
257
|
+
.map((k) => {
|
|
258
|
+
// A nullable argument declares `["string","null"]`; read it as string|null.
|
|
259
|
+
const raw = props[k]?.type;
|
|
260
|
+
const type = Array.isArray(raw) ? raw.join('|') : (raw ?? 'value');
|
|
261
|
+
const one = type === 'boolean' ? `--${k}` : `--${k} <${type}>`;
|
|
262
|
+
return required.has(k) ? one : `[${one}]`;
|
|
263
|
+
});
|
|
264
|
+
return ` ${bin} ${[cliName(tool), ...params.map((p) => `<${p}>`), ...flags].join(' ')}`;
|
|
265
|
+
}
|
|
266
|
+
/** The generated half of the help: every catalog tool without a curated command. */
|
|
267
|
+
function catalogUsage(bin = 'rcs') {
|
|
268
|
+
return tools_1.TOOLS.filter((t) => !exports.CURATED.has(t.name))
|
|
269
|
+
.map((t) => `${toolUsage(t, bin)} # ${t.scope}`)
|
|
270
|
+
.join('\n');
|
|
271
|
+
}
|
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
|
-
/**
|
|
65
|
-
|
|
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:
|
|
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
|
package/dist/client.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
22
|
-
|
|
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([
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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) ??
|
|
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
|
|
15
|
-
*
|
|
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
|
|
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");
|
|
@@ -30,8 +31,7 @@ const PROTOCOL_VERSION = '2024-11-05';
|
|
|
30
31
|
* "RCS-first" in its own tool list, contradicting the docs that sent it here.
|
|
31
32
|
* Same catalog, same gateway — only the words the model reads differ.
|
|
32
33
|
*/
|
|
33
|
-
const
|
|
34
|
-
const { product: PRODUCT, channel: CHANNEL, slug: SLUG } = (0, client_1.skinLabels)(SKIN);
|
|
34
|
+
const { product: PRODUCT, channel: CHANNEL, slug: SLUG } = (0, client_1.skinLabels)((0, client_1.detectBrand)());
|
|
35
35
|
const SERVER_INFO = { name: SLUG, version: (0, client_1.readVersion)() };
|
|
36
36
|
/**
|
|
37
37
|
* Surfaced to MCP hosts as the initialize result's `instructions` string — hosts
|
|
@@ -47,7 +47,7 @@ const INSTRUCTIONS = [
|
|
|
47
47
|
' search_templates → clone_template (into your org) → render_template (preview)',
|
|
48
48
|
' → submit_template (provider approval) → send_message.',
|
|
49
49
|
'Surveys mirror it: search_surveys/create_survey → submit_survey → send_survey →',
|
|
50
|
-
'get_results.',
|
|
50
|
+
'get_results (pass the campaignId send_survey returns for that send only).',
|
|
51
51
|
'',
|
|
52
52
|
`IMPORTANT — rich ${CHANNEL} requires approval. A freshly created or cloned`,
|
|
53
53
|
'template delivers as SMS fallback ONLY until it is submitted AND',
|
package/dist/tools.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAoB,KAAK,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5E,8DAA8D;AAC9D,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;AAErE,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;;;;OAYG;IACH,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,GAAG,GAAG,GAAG,CAAC;IAC1B,4EAA4E;IAC5E,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ,CAAC;QACf,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACpC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;QACpB,oBAAoB,CAAC,EAAE,OAAO,CAAC;KAChC,CAAC;IACF,qEAAqE;IACrE,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,WAAW,CAAC;CACvD;AAmBD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,mBAAmB,8QA+BtB,CAAC;AAEX,eAAO,MAAM,KAAK,EAAE,OAAO,
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAoB,KAAK,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5E,8DAA8D;AAC9D,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;AAErE,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;;;;OAYG;IACH,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,GAAG,GAAG,GAAG,CAAC;IAC1B,4EAA4E;IAC5E,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ,CAAC;QACf,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACpC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;QACpB,oBAAoB,CAAC,EAAE,OAAO,CAAC;KAChC,CAAC;IACF,qEAAqE;IACrE,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,WAAW,CAAC;CACvD;AAmBD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,mBAAmB,8QA+BtB,CAAC;AAEX,eAAO,MAAM,KAAK,EAAE,OAAO,EAgjD1B,CAAC;AAEF,8BAA8B;AAC9B,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAE1D;AAED,yDAAyD;AACzD,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAKrD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,OAAO,EACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACzB,cAAc,CAAC,EAAE,OAAO,GACvB,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAcrB"}
|
package/dist/tools.js
CHANGED
|
@@ -101,8 +101,8 @@ exports.TOOLS = [
|
|
|
101
101
|
},
|
|
102
102
|
{
|
|
103
103
|
name: 'search_templates',
|
|
104
|
-
description: 'Search the published
|
|
105
|
-
scope: '
|
|
104
|
+
description: 'Search the published template MARKETPLACE (templates from every org, to clone). Returns normalized listings (id, name, rating, cloneCount, categoryKeys). For your OWN templates use list_templates.',
|
|
105
|
+
scope: 'campaigns:read',
|
|
106
106
|
method: 'GET',
|
|
107
107
|
path: '/templates',
|
|
108
108
|
inputSchema: {
|
|
@@ -136,10 +136,45 @@ exports.TOOLS = [
|
|
|
136
136
|
}
|
|
137
137
|
})
|
|
138
138
|
},
|
|
139
|
+
{
|
|
140
|
+
name: 'list_templates',
|
|
141
|
+
description: "List YOUR org's own message templates (not the marketplace), newest first, with channel and provider approval status (APPROVED / PENDING / REJECTED / NOT_SUBMITTED) and any rejection reason. Filter by text, channel or approvalStatus. Archived templates only with includeArchived.",
|
|
142
|
+
scope: 'campaigns:read',
|
|
143
|
+
method: 'GET',
|
|
144
|
+
path: '/my-templates',
|
|
145
|
+
inputSchema: {
|
|
146
|
+
type: 'object',
|
|
147
|
+
properties: {
|
|
148
|
+
q: { type: 'string', description: 'Free-text search over name + description.' },
|
|
149
|
+
channel: { type: 'string', enum: ['RCS', 'WHATSAPP', 'SMS'] },
|
|
150
|
+
approvalStatus: {
|
|
151
|
+
type: 'string',
|
|
152
|
+
enum: ['APPROVED', 'PENDING', 'REJECTED', 'NOT_SUBMITTED'],
|
|
153
|
+
description: 'Only templates in this provider approval state.'
|
|
154
|
+
},
|
|
155
|
+
includeArchived: { type: 'boolean', default: false },
|
|
156
|
+
page: { type: 'number', minimum: 1 },
|
|
157
|
+
pageSize: { type: 'number', minimum: 1, maximum: 100 }
|
|
158
|
+
},
|
|
159
|
+
additionalProperties: false
|
|
160
|
+
},
|
|
161
|
+
toRequest: (a) => ({
|
|
162
|
+
method: 'GET',
|
|
163
|
+
path: '/my-templates',
|
|
164
|
+
query: {
|
|
165
|
+
q: a.q,
|
|
166
|
+
channel: a.channel,
|
|
167
|
+
approvalStatus: a.approvalStatus,
|
|
168
|
+
includeArchived: a.includeArchived,
|
|
169
|
+
page: a.page,
|
|
170
|
+
pageSize: a.pageSize
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
},
|
|
139
174
|
{
|
|
140
175
|
name: 'get_template',
|
|
141
176
|
description: 'Fetch one template by id (a published marketplace listing or one your org owns).',
|
|
142
|
-
scope: '
|
|
177
|
+
scope: 'campaigns:read',
|
|
143
178
|
method: 'GET',
|
|
144
179
|
path: '/templates/:id',
|
|
145
180
|
inputSchema: {
|
|
@@ -156,7 +191,7 @@ exports.TOOLS = [
|
|
|
156
191
|
{
|
|
157
192
|
name: 'render_template',
|
|
158
193
|
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: '
|
|
194
|
+
scope: 'campaigns:read',
|
|
160
195
|
method: 'POST',
|
|
161
196
|
path: '/templates/:id/render',
|
|
162
197
|
inputSchema: {
|
|
@@ -181,7 +216,7 @@ exports.TOOLS = [
|
|
|
181
216
|
{
|
|
182
217
|
name: 'create_template',
|
|
183
218
|
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: '
|
|
219
|
+
scope: 'campaigns:write',
|
|
185
220
|
method: 'POST',
|
|
186
221
|
path: '/templates',
|
|
187
222
|
successStatus: 201,
|
|
@@ -220,7 +255,7 @@ exports.TOOLS = [
|
|
|
220
255
|
{
|
|
221
256
|
name: 'update_template',
|
|
222
257
|
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: '
|
|
258
|
+
scope: 'campaigns:write',
|
|
224
259
|
method: 'PUT',
|
|
225
260
|
path: '/templates/:id',
|
|
226
261
|
inputSchema: {
|
|
@@ -265,7 +300,7 @@ exports.TOOLS = [
|
|
|
265
300
|
{
|
|
266
301
|
name: 'duplicate_template',
|
|
267
302
|
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: '
|
|
303
|
+
scope: 'campaigns:write',
|
|
269
304
|
method: 'POST',
|
|
270
305
|
path: '/templates/:id/duplicate',
|
|
271
306
|
successStatus: 201,
|
|
@@ -283,7 +318,7 @@ exports.TOOLS = [
|
|
|
283
318
|
{
|
|
284
319
|
name: 'delete_template',
|
|
285
320
|
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: '
|
|
321
|
+
scope: 'campaigns:write',
|
|
287
322
|
method: 'DELETE',
|
|
288
323
|
path: '/templates/:id',
|
|
289
324
|
inputSchema: {
|
|
@@ -318,7 +353,7 @@ exports.TOOLS = [
|
|
|
318
353
|
{
|
|
319
354
|
name: 'submit_template',
|
|
320
355
|
description: 'Submit an org-owned template to the provider (Twilio) for approval. Returns the contentSid + approval status.',
|
|
321
|
-
scope: '
|
|
356
|
+
scope: 'campaigns:submit',
|
|
322
357
|
method: 'POST',
|
|
323
358
|
path: '/templates/:id/submit',
|
|
324
359
|
inputSchema: {
|
|
@@ -523,7 +558,7 @@ exports.TOOLS = [
|
|
|
523
558
|
},
|
|
524
559
|
{
|
|
525
560
|
name: 'send_survey',
|
|
526
|
-
description: 'Send a survey to explicit E.164 numbers (the CRM-triggered single-customer send) and/or saved audiences. Flips the survey ACTIVE. Bills your org. Returns { recipients, sent }.',
|
|
561
|
+
description: 'Send a survey to explicit E.164 numbers (the CRM-triggered single-customer send) and/or saved audiences. Flips the survey ACTIVE. Bills your org. Returns { recipients, sent }. Every send is a campaign (WhatsApp sends the approved Flow, RCS the question-by-question run) and also returns campaignId and queued: sending: true means the rest is still going out in the background (do not send again), and halted / heldByAllowance / groundStopped say why a send stopped short.',
|
|
527
562
|
scope: 'surveys:send',
|
|
528
563
|
method: 'POST',
|
|
529
564
|
path: '/surveys/:id/send',
|
|
@@ -630,19 +665,26 @@ exports.TOOLS = [
|
|
|
630
665
|
},
|
|
631
666
|
{
|
|
632
667
|
name: 'get_results',
|
|
633
|
-
description: 'Read a survey’s aggregated results: per-question option counts / rating averages / text samples + completion totals (multi-part questions aggregate per part; free-text answers are auto-translated to English).',
|
|
668
|
+
description: 'Read a survey’s aggregated results: per-question option counts / rating averages / text samples + completion totals (multi-part questions aggregate per part; free-text answers are auto-translated to English). Pass campaignId (returned by send_survey, or a SURVEY campaign’s id from list_campaigns) for that one send’s results; without it the totals span the whole survey, test sends and every campaign included.',
|
|
634
669
|
scope: 'results:read',
|
|
635
670
|
method: 'GET',
|
|
636
671
|
path: '/surveys/:id/results',
|
|
637
672
|
inputSchema: {
|
|
638
673
|
type: 'object',
|
|
639
|
-
properties: {
|
|
674
|
+
properties: {
|
|
675
|
+
id: { type: 'string' },
|
|
676
|
+
campaignId: {
|
|
677
|
+
type: 'string',
|
|
678
|
+
description: 'Only the responses answered from this survey campaign (24-hex id). A campaign that is not this survey’s is a 404.'
|
|
679
|
+
}
|
|
680
|
+
},
|
|
640
681
|
required: ['id'],
|
|
641
682
|
additionalProperties: false
|
|
642
683
|
},
|
|
643
684
|
toRequest: (a) => ({
|
|
644
685
|
method: 'GET',
|
|
645
|
-
path: `/surveys/${encodeURIComponent(a.id)}/results
|
|
686
|
+
path: `/surveys/${encodeURIComponent(a.id)}/results`,
|
|
687
|
+
query: { campaignId: a.campaignId }
|
|
646
688
|
})
|
|
647
689
|
},
|
|
648
690
|
{
|
|
@@ -685,7 +727,7 @@ exports.TOOLS = [
|
|
|
685
727
|
{
|
|
686
728
|
name: 'get_knowledge',
|
|
687
729
|
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: '
|
|
730
|
+
scope: 'campaigns:read',
|
|
689
731
|
method: 'GET',
|
|
690
732
|
path: '/templates/:id/knowledge',
|
|
691
733
|
inputSchema: {
|
|
@@ -702,7 +744,7 @@ exports.TOOLS = [
|
|
|
702
744
|
{
|
|
703
745
|
name: 'set_knowledge',
|
|
704
746
|
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: '
|
|
747
|
+
scope: 'campaigns:write',
|
|
706
748
|
method: 'PUT',
|
|
707
749
|
path: '/templates/:id/knowledge',
|
|
708
750
|
inputSchema: {
|
|
@@ -726,7 +768,7 @@ exports.TOOLS = [
|
|
|
726
768
|
{
|
|
727
769
|
name: 'delete_knowledge',
|
|
728
770
|
description: 'Delete a template\'s knowledge base document.',
|
|
729
|
-
scope: '
|
|
771
|
+
scope: 'campaigns:write',
|
|
730
772
|
method: 'DELETE',
|
|
731
773
|
path: '/templates/:id/knowledge',
|
|
732
774
|
inputSchema: {
|
|
@@ -743,7 +785,7 @@ exports.TOOLS = [
|
|
|
743
785
|
{
|
|
744
786
|
name: 'enable_knowledge',
|
|
745
787
|
description: 'Enable knowledge answering on a template (the doc is used again).',
|
|
746
|
-
scope: '
|
|
788
|
+
scope: 'campaigns:write',
|
|
747
789
|
method: 'POST',
|
|
748
790
|
path: '/templates/:id/knowledge/enable',
|
|
749
791
|
inputSchema: {
|
|
@@ -760,7 +802,7 @@ exports.TOOLS = [
|
|
|
760
802
|
{
|
|
761
803
|
name: 'disable_knowledge',
|
|
762
804
|
description: 'Disable knowledge answering on a template WITHOUT deleting the doc (pause).',
|
|
763
|
-
scope: '
|
|
805
|
+
scope: 'campaigns:write',
|
|
764
806
|
method: 'POST',
|
|
765
807
|
path: '/templates/:id/knowledge/disable',
|
|
766
808
|
inputSchema: {
|
|
@@ -930,6 +972,54 @@ exports.TOOLS = [
|
|
|
930
972
|
body: { contacts: a.contacts }
|
|
931
973
|
})
|
|
932
974
|
},
|
|
975
|
+
{
|
|
976
|
+
name: 'update_audience',
|
|
977
|
+
description: 'Rename an audience or change its description (null or an empty string clears it). Only the fields you pass change; its contacts are untouched.',
|
|
978
|
+
scope: 'audiences:write',
|
|
979
|
+
method: 'PATCH',
|
|
980
|
+
path: '/audiences/:id',
|
|
981
|
+
inputSchema: {
|
|
982
|
+
type: 'object',
|
|
983
|
+
properties: {
|
|
984
|
+
id: { type: 'string' },
|
|
985
|
+
name: { type: 'string', minLength: 1, maxLength: 120 },
|
|
986
|
+
description: { type: ['string', 'null'], maxLength: 500 }
|
|
987
|
+
},
|
|
988
|
+
required: ['id'],
|
|
989
|
+
additionalProperties: false
|
|
990
|
+
},
|
|
991
|
+
toRequest: (a) => ({
|
|
992
|
+
method: 'PATCH',
|
|
993
|
+
path: `/audiences/${encodeURIComponent(a.id)}`,
|
|
994
|
+
body: { name: a.name, description: a.description }
|
|
995
|
+
})
|
|
996
|
+
},
|
|
997
|
+
{
|
|
998
|
+
name: 'remove_audience_contacts',
|
|
999
|
+
description: "Take people off an audience, by each row's `id` from list_audience_contacts (its `contactId` works too). This is not an opt-out: the person, their message history and their other audiences are untouched, and ids not in the audience are ignored. Returns the audience with its new contactCount and how many were removed.",
|
|
1000
|
+
scope: 'audiences:write',
|
|
1001
|
+
method: 'POST',
|
|
1002
|
+
path: '/audiences/:id/contacts/remove',
|
|
1003
|
+
inputSchema: {
|
|
1004
|
+
type: 'object',
|
|
1005
|
+
properties: {
|
|
1006
|
+
id: { type: 'string' },
|
|
1007
|
+
contactIds: {
|
|
1008
|
+
type: 'array',
|
|
1009
|
+
items: { type: 'string' },
|
|
1010
|
+
minItems: 1,
|
|
1011
|
+
maxItems: 500
|
|
1012
|
+
}
|
|
1013
|
+
},
|
|
1014
|
+
required: ['id', 'contactIds'],
|
|
1015
|
+
additionalProperties: false
|
|
1016
|
+
},
|
|
1017
|
+
toRequest: (a) => ({
|
|
1018
|
+
method: 'POST',
|
|
1019
|
+
path: `/audiences/${encodeURIComponent(a.id)}/contacts/remove`,
|
|
1020
|
+
body: { contactIds: a.contactIds }
|
|
1021
|
+
})
|
|
1022
|
+
},
|
|
933
1023
|
{
|
|
934
1024
|
name: 'delete_audience',
|
|
935
1025
|
description: 'Delete an audience and all of its contacts (org-scoped, irreversible).',
|
|
@@ -1293,6 +1383,227 @@ exports.TOOLS = [
|
|
|
1293
1383
|
tags: a.tags
|
|
1294
1384
|
}
|
|
1295
1385
|
})
|
|
1386
|
+
},
|
|
1387
|
+
// ── Campaign reads (the Messages list + the Analytics page, aggregate only) ──
|
|
1388
|
+
{
|
|
1389
|
+
name: 'list_campaigns',
|
|
1390
|
+
description: "List YOUR org's campaigns (broadcasts), newest first, with status (DRAFT / SCHEDULED / SENDING / SENT / FAILED / CANCELLED), channel, the template sent, audience count, schedule (scheduledAt in the campaign's own timezone) and the delivery funnel (total, delivered, read, failed, fellBackToSms, clicked). Archived campaigns are hidden. Read-only: creating, scheduling or sending a campaign happens in the web app.",
|
|
1391
|
+
scope: 'campaigns:read',
|
|
1392
|
+
method: 'GET',
|
|
1393
|
+
path: '/campaigns',
|
|
1394
|
+
inputSchema: {
|
|
1395
|
+
type: 'object',
|
|
1396
|
+
properties: {
|
|
1397
|
+
q: { type: 'string', maxLength: 120, description: 'Free-text search over the campaign name.' },
|
|
1398
|
+
status: {
|
|
1399
|
+
type: 'string',
|
|
1400
|
+
enum: ['DRAFT', 'SCHEDULED', 'SENDING', 'SENT', 'FAILED', 'CANCELLED']
|
|
1401
|
+
},
|
|
1402
|
+
channel: { type: 'string', enum: ['RCS', 'WHATSAPP'] },
|
|
1403
|
+
page: { type: 'number', minimum: 1 },
|
|
1404
|
+
pageSize: { type: 'number', minimum: 1, maximum: 100 }
|
|
1405
|
+
},
|
|
1406
|
+
additionalProperties: false
|
|
1407
|
+
},
|
|
1408
|
+
toRequest: (a) => ({
|
|
1409
|
+
method: 'GET',
|
|
1410
|
+
path: '/campaigns',
|
|
1411
|
+
query: {
|
|
1412
|
+
q: a.q,
|
|
1413
|
+
status: a.status,
|
|
1414
|
+
channel: a.channel,
|
|
1415
|
+
page: a.page,
|
|
1416
|
+
pageSize: a.pageSize
|
|
1417
|
+
}
|
|
1418
|
+
})
|
|
1419
|
+
},
|
|
1420
|
+
{
|
|
1421
|
+
name: 'get_campaign_analytics',
|
|
1422
|
+
description: "Campaign analytics, the same figures as the web app's Analytics page: totals and rates (delivered, read, clicked, failed, fell back to SMS) over the last N days (default 30, max 365), a daily series (UTC days; hourly for a 1-day window), failure classes by provider error code, after-delivery outcomes (replied, opportunity, not interested) and a per-campaign table. Pass broadcastId (a list_campaigns id) to narrow every figure to one campaign over its own lifetime. Aggregate only — no phone numbers or message text.",
|
|
1423
|
+
scope: 'campaigns:read',
|
|
1424
|
+
method: 'GET',
|
|
1425
|
+
path: '/analytics',
|
|
1426
|
+
inputSchema: {
|
|
1427
|
+
type: 'object',
|
|
1428
|
+
properties: {
|
|
1429
|
+
channel: {
|
|
1430
|
+
type: 'string',
|
|
1431
|
+
enum: ['RCS', 'WHATSAPP'],
|
|
1432
|
+
description: 'Only this channel. Omit for every campaign.'
|
|
1433
|
+
},
|
|
1434
|
+
days: { type: 'number', minimum: 1, maximum: 365 },
|
|
1435
|
+
broadcastId: { type: 'string', description: 'One campaign (its list_campaigns id).' }
|
|
1436
|
+
},
|
|
1437
|
+
additionalProperties: false
|
|
1438
|
+
},
|
|
1439
|
+
toRequest: (a) => ({
|
|
1440
|
+
method: 'GET',
|
|
1441
|
+
path: '/analytics',
|
|
1442
|
+
query: { channel: a.channel, days: a.days, broadcastId: a.broadcastId }
|
|
1443
|
+
})
|
|
1444
|
+
},
|
|
1445
|
+
// ── Campaign groups (named sets of campaigns — bookkeeping, never a send) ──
|
|
1446
|
+
{
|
|
1447
|
+
name: 'list_campaign_groups',
|
|
1448
|
+
description: "List YOUR org's campaign groups — named sets of campaigns filed together (an event, a launch, a quarter) — most recently changed first, each with its member campaign ids, campaign count and combined delivery funnel (every member's funnel added together, from cached figures). Pass broadcastId (a list_campaigns id) for only the groups that campaign is in.",
|
|
1449
|
+
scope: 'campaigns:read',
|
|
1450
|
+
method: 'GET',
|
|
1451
|
+
path: '/campaign-groups',
|
|
1452
|
+
inputSchema: {
|
|
1453
|
+
type: 'object',
|
|
1454
|
+
properties: {
|
|
1455
|
+
broadcastId: {
|
|
1456
|
+
type: 'string',
|
|
1457
|
+
description: 'Only the groups this campaign (a list_campaigns id) is in.'
|
|
1458
|
+
}
|
|
1459
|
+
},
|
|
1460
|
+
additionalProperties: false
|
|
1461
|
+
},
|
|
1462
|
+
toRequest: (a) => ({
|
|
1463
|
+
method: 'GET',
|
|
1464
|
+
path: '/campaign-groups',
|
|
1465
|
+
query: { broadcastId: a.broadcastId }
|
|
1466
|
+
})
|
|
1467
|
+
},
|
|
1468
|
+
{
|
|
1469
|
+
name: 'get_campaign_group',
|
|
1470
|
+
description: 'One campaign group: its member campaigns (status, channel, schedule and each funnel, refreshed when stale) and their combined funnel. Campaigns deleted since they were filed are left out.',
|
|
1471
|
+
scope: 'campaigns:read',
|
|
1472
|
+
method: 'GET',
|
|
1473
|
+
path: '/campaign-groups/:id',
|
|
1474
|
+
inputSchema: {
|
|
1475
|
+
type: 'object',
|
|
1476
|
+
properties: { id: { type: 'string' } },
|
|
1477
|
+
required: ['id'],
|
|
1478
|
+
additionalProperties: false
|
|
1479
|
+
},
|
|
1480
|
+
toRequest: (a) => ({
|
|
1481
|
+
method: 'GET',
|
|
1482
|
+
path: `/campaign-groups/${encodeURIComponent(a.id)}`
|
|
1483
|
+
})
|
|
1484
|
+
},
|
|
1485
|
+
{
|
|
1486
|
+
name: 'create_campaign_group',
|
|
1487
|
+
description: 'Create a campaign group, optionally filing campaigns (list_campaigns ids) in it at once. Names are unique per org, ignoring case and spacing. A group never changes who a campaign reaches or when it sends; a campaign may be in several groups.',
|
|
1488
|
+
scope: 'campaigns:write',
|
|
1489
|
+
method: 'POST',
|
|
1490
|
+
path: '/campaign-groups',
|
|
1491
|
+
successStatus: 201,
|
|
1492
|
+
inputSchema: {
|
|
1493
|
+
type: 'object',
|
|
1494
|
+
properties: {
|
|
1495
|
+
name: { type: 'string', minLength: 1, maxLength: 120 },
|
|
1496
|
+
description: { type: 'string', maxLength: 500 },
|
|
1497
|
+
broadcastIds: {
|
|
1498
|
+
type: 'array',
|
|
1499
|
+
items: { type: 'string' },
|
|
1500
|
+
maxItems: 500,
|
|
1501
|
+
description: 'Campaigns (list_campaigns ids) to start the group with.'
|
|
1502
|
+
}
|
|
1503
|
+
},
|
|
1504
|
+
required: ['name'],
|
|
1505
|
+
additionalProperties: false
|
|
1506
|
+
},
|
|
1507
|
+
toRequest: (a) => ({
|
|
1508
|
+
method: 'POST',
|
|
1509
|
+
path: '/campaign-groups',
|
|
1510
|
+
body: {
|
|
1511
|
+
name: a.name,
|
|
1512
|
+
description: a.description,
|
|
1513
|
+
broadcastIds: a.broadcastIds
|
|
1514
|
+
}
|
|
1515
|
+
})
|
|
1516
|
+
},
|
|
1517
|
+
{
|
|
1518
|
+
name: 'update_campaign_group',
|
|
1519
|
+
description: "Rename a campaign group or change its description (null clears it). Only the fields you pass change; use add_campaigns_to_group / remove_campaigns_from_group to change its campaigns.",
|
|
1520
|
+
scope: 'campaigns:write',
|
|
1521
|
+
method: 'PATCH',
|
|
1522
|
+
path: '/campaign-groups/:id',
|
|
1523
|
+
inputSchema: {
|
|
1524
|
+
type: 'object',
|
|
1525
|
+
properties: {
|
|
1526
|
+
id: { type: 'string' },
|
|
1527
|
+
name: { type: 'string', minLength: 1, maxLength: 120 },
|
|
1528
|
+
description: { type: ['string', 'null'], maxLength: 500 }
|
|
1529
|
+
},
|
|
1530
|
+
required: ['id'],
|
|
1531
|
+
additionalProperties: false
|
|
1532
|
+
},
|
|
1533
|
+
toRequest: (a) => ({
|
|
1534
|
+
method: 'PATCH',
|
|
1535
|
+
path: `/campaign-groups/${encodeURIComponent(a.id)}`,
|
|
1536
|
+
body: { name: a.name, description: a.description }
|
|
1537
|
+
})
|
|
1538
|
+
},
|
|
1539
|
+
{
|
|
1540
|
+
name: 'delete_campaign_group',
|
|
1541
|
+
description: 'Delete a campaign group. Only the group goes: its campaigns, their recipients and their results are untouched.',
|
|
1542
|
+
scope: 'campaigns:write',
|
|
1543
|
+
method: 'DELETE',
|
|
1544
|
+
path: '/campaign-groups/:id',
|
|
1545
|
+
inputSchema: {
|
|
1546
|
+
type: 'object',
|
|
1547
|
+
properties: { id: { type: 'string' } },
|
|
1548
|
+
required: ['id'],
|
|
1549
|
+
additionalProperties: false
|
|
1550
|
+
},
|
|
1551
|
+
toRequest: (a) => ({
|
|
1552
|
+
method: 'DELETE',
|
|
1553
|
+
path: `/campaign-groups/${encodeURIComponent(a.id)}`
|
|
1554
|
+
})
|
|
1555
|
+
},
|
|
1556
|
+
{
|
|
1557
|
+
name: 'add_campaigns_to_group',
|
|
1558
|
+
description: 'File campaigns (list_campaigns ids) in a campaign group. Idempotent: a campaign already in the group stays where it is. Returns the group with its campaigns.',
|
|
1559
|
+
scope: 'campaigns:write',
|
|
1560
|
+
method: 'POST',
|
|
1561
|
+
path: '/campaign-groups/:id/campaigns',
|
|
1562
|
+
inputSchema: {
|
|
1563
|
+
type: 'object',
|
|
1564
|
+
properties: {
|
|
1565
|
+
id: { type: 'string' },
|
|
1566
|
+
broadcastIds: {
|
|
1567
|
+
type: 'array',
|
|
1568
|
+
items: { type: 'string' },
|
|
1569
|
+
minItems: 1,
|
|
1570
|
+
maxItems: 500
|
|
1571
|
+
}
|
|
1572
|
+
},
|
|
1573
|
+
required: ['id', 'broadcastIds'],
|
|
1574
|
+
additionalProperties: false
|
|
1575
|
+
},
|
|
1576
|
+
toRequest: (a) => ({
|
|
1577
|
+
method: 'POST',
|
|
1578
|
+
path: `/campaign-groups/${encodeURIComponent(a.id)}/campaigns`,
|
|
1579
|
+
body: { broadcastIds: a.broadcastIds }
|
|
1580
|
+
})
|
|
1581
|
+
},
|
|
1582
|
+
{
|
|
1583
|
+
name: 'remove_campaigns_from_group',
|
|
1584
|
+
description: 'Take campaigns out of a campaign group. The campaigns themselves are untouched; ids not in the group are ignored. Returns the group with its campaigns.',
|
|
1585
|
+
scope: 'campaigns:write',
|
|
1586
|
+
method: 'POST',
|
|
1587
|
+
path: '/campaign-groups/:id/campaigns/remove',
|
|
1588
|
+
inputSchema: {
|
|
1589
|
+
type: 'object',
|
|
1590
|
+
properties: {
|
|
1591
|
+
id: { type: 'string' },
|
|
1592
|
+
broadcastIds: {
|
|
1593
|
+
type: 'array',
|
|
1594
|
+
items: { type: 'string' },
|
|
1595
|
+
minItems: 1,
|
|
1596
|
+
maxItems: 500
|
|
1597
|
+
}
|
|
1598
|
+
},
|
|
1599
|
+
required: ['id', 'broadcastIds'],
|
|
1600
|
+
additionalProperties: false
|
|
1601
|
+
},
|
|
1602
|
+
toRequest: (a) => ({
|
|
1603
|
+
method: 'POST',
|
|
1604
|
+
path: `/campaign-groups/${encodeURIComponent(a.id)}/campaigns/remove`,
|
|
1605
|
+
body: { broadcastIds: a.broadcastIds }
|
|
1606
|
+
})
|
|
1296
1607
|
}
|
|
1297
1608
|
];
|
|
1298
1609
|
/** Look up a tool by name. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rcs-campaigns/agent-toolkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
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",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"scripts": {
|
|
64
64
|
"build": "tsc",
|
|
65
65
|
"dev": "tsc --watch",
|
|
66
|
-
"typecheck": "tsc --noEmit",
|
|
66
|
+
"typecheck": "tsc --noEmit --incremental --tsBuildInfoFile node_modules/.cache/tsc/typecheck.tsbuildinfo",
|
|
67
67
|
"test": "vitest run",
|
|
68
68
|
"test:watch": "vitest"
|
|
69
69
|
}
|