@rcs-campaigns/agent-toolkit 0.1.5

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/dist/client.js ADDED
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GatewayClient = exports.GatewayError = void 0;
4
+ exports.detectSkin = detectSkin;
5
+ exports.readVersion = readVersion;
6
+ exports.skinLabels = skinLabels;
7
+ exports.normalizeChannel = normalizeChannel;
8
+ exports.resolveConfig = resolveConfig;
9
+ const node_fs_1 = require("node:fs");
10
+ const node_path_1 = require("node:path");
11
+ /**
12
+ * Which skin this process was launched as, from the bin name in argv.
13
+ *
14
+ * `wa`/`wa-mcp` mean the caller came in through the WhatsApp-branded docs and
15
+ * expects WhatsApp unless they say otherwise. Anything else (including being
16
+ * imported as a library) is treated as the neutral RCS skin, which leaves the
17
+ * channel to the server. Detection is deliberately conservative: an unknown
18
+ * argv, a bundler-rewritten path, or a direct import all fall through to 'rcs'.
19
+ */
20
+ function detectSkin(argv = process.argv) {
21
+ const invoked = argv[1];
22
+ if (typeof invoked !== 'string')
23
+ return 'rcs';
24
+ // basename without extension, so ".../bin/wa-mcp.js" → "wa-mcp".
25
+ const base = invoked.split(/[\\/]/).pop() ?? '';
26
+ const name = base.replace(/\.(js|cjs|mjs|ts)$/, '');
27
+ return name === 'wa' || name === 'wa-mcp' ? 'wa' : 'rcs';
28
+ }
29
+ /**
30
+ * The package's own version, read at runtime rather than restated in a
31
+ * constant. A hardcoded copy silently goes stale the first time anyone bumps
32
+ * for a release, and both skins show this string to a user. From dist/ and from
33
+ * src/ alike the package root is one level up.
34
+ */
35
+ function readVersion() {
36
+ try {
37
+ const pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, '..', 'package.json'), 'utf8'));
38
+ return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
39
+ }
40
+ catch {
41
+ return '0.0.0';
42
+ }
43
+ }
44
+ /** Human-readable product + channel names for the running skin. */
45
+ function skinLabels(skin = detectSkin()) {
46
+ return skin === 'wa'
47
+ ? { product: 'WhatsApp Campaigns', channel: 'WhatsApp', slug: 'wa-campaigns' }
48
+ : { product: 'RCS Campaigns', channel: 'RCS', slug: 'rcs-campaigns' };
49
+ }
50
+ /** First environment variable that is set and non-empty, under either prefix. */
51
+ function fromEnv(...names) {
52
+ for (const name of names) {
53
+ const value = process.env[name];
54
+ if (value)
55
+ return value;
56
+ }
57
+ return undefined;
58
+ }
59
+ /**
60
+ * Brand ids a user may type where a channel is expected (MSG-1867 phase 3).
61
+ * CANONICAL ids first, then the LEGACY spellings that are still persisted in
62
+ * keys, org metadata and old docs until the brand-id data migration has run
63
+ * everywhere. Mirrors `BRAND_IDS` / `BRAND_ID_ALIASES` in
64
+ * `@msg2ai/template-core`; this package is zero-dependency and published to
65
+ * npm, so it cannot import that registry.
66
+ */
67
+ const BRAND_CHANNELS = {
68
+ 'rcs-campaigns': 'RCS',
69
+ 'rcs-surveys': 'RCS',
70
+ 'wa-campaigns': 'WHATSAPP',
71
+ 'wa-surveys': 'WHATSAPP',
72
+ // legacy spellings — accepted, never emitted
73
+ 'rcs-templates': 'RCS',
74
+ 'wa-templates': 'WHATSAPP'
75
+ };
76
+ /**
77
+ * Normalize a channel from configuration or a tool argument. Accepts the channel
78
+ * enum itself (any case) AND a brand id — canonical or legacy — resolving it to
79
+ * that brand's channel. Always returns the canonical channel enum; unknown
80
+ * values are ignored.
81
+ */
82
+ function normalizeChannel(value) {
83
+ if (typeof value !== 'string')
84
+ return undefined;
85
+ const v = value.trim();
86
+ const upper = v.toUpperCase();
87
+ if (upper === 'RCS' || upper === 'WHATSAPP')
88
+ return upper;
89
+ return BRAND_CHANNELS[v.toLowerCase()];
90
+ }
91
+ /** Normalize a channel from configuration; unknown values are ignored. */
92
+ function readChannel(value) {
93
+ return normalizeChannel(value);
94
+ }
95
+ /** A gateway error carrying the HTTP status + the server's error envelope. */
96
+ class GatewayError extends Error {
97
+ status;
98
+ details;
99
+ constructor(message, status, details) {
100
+ super(message);
101
+ this.name = 'GatewayError';
102
+ this.status = status;
103
+ this.details = details;
104
+ }
105
+ }
106
+ exports.GatewayError = GatewayError;
107
+ /**
108
+ * Resolve config from explicit values, falling back to the environment.
109
+ *
110
+ * WA_* and RCS_* are accepted interchangeably. When both are set the one
111
+ * matching the running skin wins, so a machine configured for both does the
112
+ * expected thing under either bin.
113
+ */
114
+ function resolveConfig(partial) {
115
+ const skin = detectSkin();
116
+ const [urlFirst, urlSecond] = skin === 'wa'
117
+ ? ['WA_GATEWAY_URL', 'RCS_GATEWAY_URL']
118
+ : ['RCS_GATEWAY_URL', 'WA_GATEWAY_URL'];
119
+ const [keyFirst, keySecond] = skin === 'wa'
120
+ ? ['WA_AGENT_KEY', 'RCS_AGENT_KEY']
121
+ : ['RCS_AGENT_KEY', 'WA_AGENT_KEY'];
122
+ const baseUrl = partial?.baseUrl ?? fromEnv(urlFirst, urlSecond) ?? 'http://localhost:3002';
123
+ const apiKey = partial?.apiKey ?? fromEnv(keyFirst, keySecond) ?? '';
124
+ if (!apiKey) {
125
+ throw new Error(`No agent key configured. Set ${keyFirst} or pass apiKey explicitly.`);
126
+ }
127
+ // Explicit beats env beats skin default. The wa skin assumes WhatsApp; the rcs
128
+ // skin stays silent and lets the server pick the org's own brand channel.
129
+ const defaultChannel = partial?.defaultChannel ??
130
+ readChannel(fromEnv('WA_DEFAULT_CHANNEL', 'RCS_DEFAULT_CHANNEL')) ??
131
+ (skin === 'wa' ? 'WHATSAPP' : undefined);
132
+ return { baseUrl: baseUrl.replace(/\/$/, ''), apiKey, defaultChannel };
133
+ }
134
+ class GatewayClient {
135
+ config;
136
+ constructor(config) {
137
+ this.config = config;
138
+ }
139
+ static fromEnv(partial) {
140
+ return new GatewayClient(resolveConfig(partial));
141
+ }
142
+ /** Issue a request against the gateway and return `data` from the envelope. */
143
+ async call(spec) {
144
+ const url = new URL(`${this.config.baseUrl}/api/agent${spec.path}`);
145
+ for (const [k, v] of Object.entries(spec.query ?? {})) {
146
+ if (v !== undefined && v !== '')
147
+ url.searchParams.set(k, String(v));
148
+ }
149
+ const res = await fetch(url, {
150
+ method: spec.method,
151
+ headers: {
152
+ Authorization: `Bearer ${this.config.apiKey}`,
153
+ ...(spec.body !== undefined
154
+ ? { 'Content-Type': 'application/json' }
155
+ : {})
156
+ },
157
+ body: spec.body !== undefined ? JSON.stringify(spec.body) : undefined
158
+ });
159
+ const text = await res.text();
160
+ let parsed = undefined;
161
+ try {
162
+ parsed = text ? JSON.parse(text) : undefined;
163
+ }
164
+ catch {
165
+ // Non-JSON body (e.g. a proxy error page); surface the raw text.
166
+ if (!res.ok)
167
+ throw new GatewayError(text || res.statusText, res.status);
168
+ }
169
+ if (!res.ok || parsed?.success === false) {
170
+ throw new GatewayError(parsed?.error ?? `Request failed (${res.status})`, res.status, parsed?.details);
171
+ }
172
+ return (parsed?.data ?? parsed);
173
+ }
174
+ }
175
+ exports.GatewayClient = GatewayClient;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @rcs-campaigns/agent-toolkit
3
+ *
4
+ * Two agent skins over one gateway client:
5
+ * - the MCP server (`rcs-mcp` bin, ./mcp)
6
+ * - the CLI (`rcs` bin, ./cli)
7
+ * sharing the typed gateway client (./client) and the declarative tool catalog
8
+ * (./tools). Importing the package gives you the client + catalog to embed in
9
+ * your own agent; the bins are the ready-made skins.
10
+ *
11
+ * @see docs/design/AGENT-ENABLEMENT-AND-X402-DESIGN.md
12
+ */
13
+ export * from './client';
14
+ export * from './tools';
15
+ export * from './openapi';
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ /**
18
+ * @rcs-campaigns/agent-toolkit
19
+ *
20
+ * Two agent skins over one gateway client:
21
+ * - the MCP server (`rcs-mcp` bin, ./mcp)
22
+ * - the CLI (`rcs` bin, ./cli)
23
+ * sharing the typed gateway client (./client) and the declarative tool catalog
24
+ * (./tools). Importing the package gives you the client + catalog to embed in
25
+ * your own agent; the bins are the ready-made skins.
26
+ *
27
+ * @see docs/design/AGENT-ENABLEMENT-AND-X402-DESIGN.md
28
+ */
29
+ __exportStar(require("./client"), exports);
30
+ __exportStar(require("./tools"), exports);
31
+ __exportStar(require("./openapi"), exports);
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=mcp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":""}
package/dist/mcp.js ADDED
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ /**
5
+ * `rcs-mcp` — a Model Context Protocol (MCP) server exposing the RCS Campaigns
6
+ * agent gateway as tools, so Claude (and any MCP-capable agent) can search, read,
7
+ * render, create, clone, submit, and send RCS templates.
8
+ *
9
+ * Transport: stdio with newline-delimited JSON-RPC 2.0 (the MCP stdio framing).
10
+ * It is implemented by hand against the protocol so this package carries ZERO
11
+ * runtime dependencies — nothing to install, nothing to drift, and it slots into
12
+ * the workspace without touching the lockfile's external graph.
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
16
+ * it as a Bearer token to the gateway — all business logic stays server-side.
17
+ *
18
+ * Wire it up (e.g. Claude Code):
19
+ * 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
+ */
22
+ const node_readline_1 = require("node:readline");
23
+ const client_1 = require("./client");
24
+ const tools_1 = require("./tools");
25
+ const PROTOCOL_VERSION = '2024-11-05';
26
+ /**
27
+ * The MCP host shows `serverInfo` and `instructions` to the user and injects the
28
+ * latter as system guidance. Both must match the skin the server was launched
29
+ * as: an agent that registered `wa-mcp` should not find "RCS Campaigns" and
30
+ * "RCS-first" in its own tool list, contradicting the docs that sent it here.
31
+ * Same catalog, same gateway — only the words the model reads differ.
32
+ */
33
+ const SKIN = (0, client_1.detectSkin)();
34
+ const { product: PRODUCT, channel: CHANNEL, slug: SLUG } = (0, client_1.skinLabels)(SKIN);
35
+ const SERVER_INFO = { name: SLUG, version: (0, client_1.readVersion)() };
36
+ /**
37
+ * Surfaced to MCP hosts as the initialize result's `instructions` string — hosts
38
+ * inject it as system guidance for the model. It teaches the canonical workflow
39
+ * so an agent does not have to discover the submit-before-RCS step by trial:
40
+ * without it, sends silently fall back to SMS and the agent has no cue why.
41
+ */
42
+ const INSTRUCTIONS = [
43
+ `${PRODUCT} agent gateway — author, approve, and send ${CHANNEL}-first message`,
44
+ 'templates and conversational surveys over one typed contract.',
45
+ '',
46
+ 'Canonical template flow:',
47
+ ' search_templates → clone_template (into your org) → render_template (preview)',
48
+ ' → submit_template (provider approval) → send_message.',
49
+ 'Surveys mirror it: search_surveys/create_survey → submit_survey → send_survey →',
50
+ 'get_results.',
51
+ '',
52
+ `IMPORTANT — rich ${CHANNEL} requires approval. A freshly created or cloned`,
53
+ 'template delivers as SMS fallback ONLY until it is submitted AND',
54
+ 'provider-approved. send_message returns a funnel whose `fellBackToSms` count',
55
+ `is > 0 whenever the rich ${CHANNEL} path was unavailable — submit the template`,
56
+ `and wait for approval to deliver rich ${CHANNEL}. send_message/send_survey bill`,
57
+ 'your org and are irreversible.',
58
+ '',
59
+ 'Call `capabilities` first to see the tool catalog and which scopes your key holds.'
60
+ ].join('\n');
61
+ /** Write one newline-delimited JSON-RPC message to stdout. */
62
+ function send(msg) {
63
+ process.stdout.write(`${JSON.stringify(msg)}\n`);
64
+ }
65
+ function reply(id, result) {
66
+ send({ jsonrpc: '2.0', id, result });
67
+ }
68
+ function replyError(id, code, message, data) {
69
+ send({ jsonrpc: '2.0', id, error: { code, message, data } });
70
+ }
71
+ // Lazily build the client so `tools/list` works even before a key is set, but a
72
+ // `tools/call` without a key fails cleanly with a helpful message.
73
+ let client = null;
74
+ let config = null;
75
+ function getClient() {
76
+ if (!client) {
77
+ config = (0, client_1.resolveConfig)();
78
+ client = new client_1.GatewayClient(config);
79
+ }
80
+ return client;
81
+ }
82
+ async function handleToolCall(id, params) {
83
+ const name = params?.name;
84
+ const args = params?.arguments ?? {};
85
+ const tool = (0, tools_1.findTool)(name);
86
+ if (!tool) {
87
+ return replyError(id, -32602, `Unknown tool: ${name}`);
88
+ }
89
+ try {
90
+ const gateway = getClient();
91
+ // Resolve the skin's default channel before mapping args to a request, so
92
+ // `wa-mcp` sends WhatsApp without every caller repeating themselves.
93
+ const resolved = (0, tools_1.withDefaultChannel)(tool, args, config?.defaultChannel);
94
+ const data = await gateway.call(tool.toRequest(resolved));
95
+ // MCP tool results are content blocks; we return the JSON payload as text so
96
+ // the agent can parse it. isError stays false on success.
97
+ reply(id, {
98
+ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
99
+ isError: false
100
+ });
101
+ }
102
+ catch (err) {
103
+ const message = err instanceof client_1.GatewayError
104
+ ? `Gateway ${err.status}: ${err.message}`
105
+ : err instanceof Error
106
+ ? err.message
107
+ : String(err);
108
+ // Surface the failure as an MCP tool error (not a protocol error) so the
109
+ // agent can read it and decide what to do (e.g. fix args, request a scope).
110
+ reply(id, {
111
+ content: [{ type: 'text', text: message }],
112
+ isError: true
113
+ });
114
+ }
115
+ }
116
+ function handleMessage(msg) {
117
+ switch (msg.method) {
118
+ case 'initialize': {
119
+ // Echo the client's requested protocol version when it sends one (the spec
120
+ // negotiates on it); fall back to the version we implement against.
121
+ const requested = msg.params?.protocolVersion;
122
+ reply(msg.id, {
123
+ protocolVersion: typeof requested === 'string' ? requested : PROTOCOL_VERSION,
124
+ capabilities: { tools: {} },
125
+ serverInfo: SERVER_INFO,
126
+ instructions: INSTRUCTIONS
127
+ });
128
+ return;
129
+ }
130
+ case 'notifications/initialized':
131
+ return; // notification: no response
132
+ case 'ping':
133
+ reply(msg.id, {});
134
+ return;
135
+ case 'tools/list':
136
+ reply(msg.id, {
137
+ tools: tools_1.TOOLS.map((t) => ({
138
+ name: t.name,
139
+ description: t.description,
140
+ inputSchema: t.inputSchema
141
+ }))
142
+ });
143
+ return;
144
+ case 'tools/call':
145
+ void handleToolCall(msg.id, msg.params);
146
+ return;
147
+ default:
148
+ // Unknown request -> method-not-found; ignore unknown notifications.
149
+ if (msg.id !== undefined && msg.id !== null) {
150
+ replyError(msg.id, -32601, `Method not found: ${msg.method}`);
151
+ }
152
+ }
153
+ }
154
+ function main() {
155
+ const rl = (0, node_readline_1.createInterface)({ input: process.stdin });
156
+ rl.on('line', (line) => {
157
+ const trimmed = line.trim();
158
+ if (!trimmed)
159
+ return;
160
+ let msg;
161
+ try {
162
+ msg = JSON.parse(trimmed);
163
+ }
164
+ catch {
165
+ return; // ignore malformed lines (no id to respond to)
166
+ }
167
+ try {
168
+ handleMessage(msg);
169
+ }
170
+ catch (err) {
171
+ if (msg.id !== undefined && msg.id !== null) {
172
+ replyError(msg.id, -32603, 'Internal error', String(err));
173
+ }
174
+ }
175
+ });
176
+ rl.on('close', () => process.exit(0));
177
+ }
178
+ main();
@@ -0,0 +1,36 @@
1
+ export interface OpenApiServer {
2
+ url: string;
3
+ description: string;
4
+ }
5
+ export interface BuildOpenApiOptions {
6
+ /**
7
+ * The webhook event types to document under the top-level `webhooks` object.
8
+ *
9
+ * Defaults to `WEBHOOK_EVENT_NAMES`, which IS the vocabulary — `apps/server`
10
+ * re-exports it as `WEBHOOK_EVENTS` rather than declaring a second list, so
11
+ * the default is already the server's answer and passing it explicitly (as
12
+ * `routes/agent.ts` does) is a no-op that documents the intent. The override
13
+ * stays for an embedder generating a document for a gateway build that
14
+ * fires a different set.
15
+ */
16
+ webhookEvents?: readonly string[];
17
+ /** Document version stamped into `info.version`. */
18
+ version?: string;
19
+ }
20
+ /** Anything JSON-serialisable. The document is data, not a typed AST. */
21
+ type Json = Record<string, any>;
22
+ /** `/conversations/:id` → `/conversations/{id}` (OpenAPI path templating). */
23
+ export declare function toOpenApiPath(path: string): string;
24
+ /** The `:param` names in an Express-style path, in order. */
25
+ export declare function pathParamsOf(path: string): string[];
26
+ /**
27
+ * Build the gateway's OpenAPI 3.1 document.
28
+ *
29
+ * `servers` is supplied by the caller because only the server knows which
30
+ * environment it is: a published spec whose host is a placeholder cannot be
31
+ * imported into Postman or a code generator without hand-editing, which is the
32
+ * exact defect this replaces.
33
+ */
34
+ export declare function buildOpenApiDocument(servers: OpenApiServer[], options?: BuildOpenApiOptions): Json;
35
+ export {};
36
+ //# sourceMappingURL=openapi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"AA0BA,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC;;;;;;;;;OASG;IACH,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,yEAAyE;AACzE,KAAK,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAMhC,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,6DAA6D;AAC7D,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAEnD;AAwfD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,aAAa,EAAE,EACxB,OAAO,GAAE,mBAAwB,GAChC,IAAI,CAmFN"}