@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.
@@ -0,0 +1,590 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toOpenApiPath = toOpenApiPath;
4
+ exports.pathParamsOf = pathParamsOf;
5
+ exports.buildOpenApiDocument = buildOpenApiDocument;
6
+ /**
7
+ * The agent gateway's OpenAPI 3.1 document, generated from `TOOLS`.
8
+ *
9
+ * Why generated, and why HERE: before MSG-1833 the public API reference was
10
+ * hand-copied into two website repos and the capabilities catalog was a
11
+ * hand-written literal in `apps/server` — four lists of the same surface, none
12
+ * of which checked the others. A route reached the docs only if somebody
13
+ * remembered to retype it twice. This module makes the catalog the source: the
14
+ * server serves `buildOpenApiDocument(...)` at `GET /api/agent/openapi.json`,
15
+ * so a tool that exists is documented and a tool that does not, is not.
16
+ *
17
+ * Pure and dependency-free by design (no `zod-to-openapi`, none is in the repo):
18
+ * `ToolDef.inputSchema` is already JSON Schema, and OpenAPI 3.1 embeds JSON
19
+ * Schema 2020-12 directly, so each tool's schema mounts near-verbatim.
20
+ *
21
+ * The one non-obvious rule is the parameter/body split:
22
+ * - a property whose name matches a `:param` in `path` becomes a PATH
23
+ * parameter (always required);
24
+ * - of the rest, GET/DELETE expand into QUERY parameters and
25
+ * POST/PUT/PATCH become the `requestBody`.
26
+ * Every current tool satisfies the invariant this rests on (every `:param` is
27
+ * also an `inputSchema` property); the toolkit's own tests pin it so a future
28
+ * tool cannot break the generator silently.
29
+ */
30
+ const tools_1 = require("./tools");
31
+ // ---------------------------------------------------------------------------
32
+ // Small pure helpers
33
+ // ---------------------------------------------------------------------------
34
+ /** `/conversations/:id` → `/conversations/{id}` (OpenAPI path templating). */
35
+ function toOpenApiPath(path) {
36
+ return path.replace(/:([A-Za-z0-9_]+)/g, '{$1}');
37
+ }
38
+ /** The `:param` names in an Express-style path, in order. */
39
+ function pathParamsOf(path) {
40
+ return [...path.matchAll(/:([A-Za-z0-9_]+)/g)].map((m) => m[1]);
41
+ }
42
+ /** `create_webhook` → `CreateWebhook`. */
43
+ function pascal(name) {
44
+ return name
45
+ .split(/[_\-\s]+/)
46
+ .filter(Boolean)
47
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
48
+ .join('');
49
+ }
50
+ /**
51
+ * The first sentence of a tool description, for `summary`.
52
+ *
53
+ * Tool descriptions are written for a model and run long (they carry the
54
+ * gotchas an agent needs). A generated reference still wants a one-line label
55
+ * per operation, and the full text stays as `description`.
56
+ */
57
+ function firstSentence(text) {
58
+ const trimmed = text.trim();
59
+ const stop = trimmed.search(/\.(\s|$)/);
60
+ const first = stop === -1 ? trimmed : trimmed.slice(0, stop + 1);
61
+ return first.length > 160 ? `${first.slice(0, 157)}…` : first;
62
+ }
63
+ /** Group a tool under a docs tag by the resource its path names. */
64
+ function tagFor(tool) {
65
+ const head = tool.path.split('/')[1] ?? '';
66
+ switch (head) {
67
+ case 'templates':
68
+ case 'marketplace':
69
+ return 'Templates';
70
+ case 'surveys':
71
+ return 'Surveys';
72
+ case 'messages':
73
+ return 'Messages';
74
+ case 'media':
75
+ return 'Media';
76
+ case 'audiences':
77
+ return 'Audiences';
78
+ case 'contacts':
79
+ return 'Contacts';
80
+ case 'conversations':
81
+ return 'Conversations';
82
+ case 'webhooks':
83
+ return 'Webhooks';
84
+ case 'usage':
85
+ return 'Usage';
86
+ default:
87
+ return 'Discovery';
88
+ }
89
+ }
90
+ /**
91
+ * The tools that reach a real phone and bill the org, and so sit behind the
92
+ * TIGHTER per-key limiter (`agentSendLimiter`) rather than the general one.
93
+ * Named here so the generated 429 description carries the number that actually
94
+ * applies to the operation rather than a single averaged claim.
95
+ */
96
+ const TIGHTLY_LIMITED = new Set([
97
+ 'send_message',
98
+ 'send_survey',
99
+ 'send_survey_test',
100
+ 'reply'
101
+ ]);
102
+ const RATE_LIMIT_NOTE = 'Rate limits: 300 requests/minute per IP before authentication, 120/minute per key after it.';
103
+ const SEND_RATE_LIMIT_NOTE = 'Rate limits: 300 requests/minute per IP before authentication, 120/minute per key after it, and 30/minute per key on this operation — it reaches a real handset and bills the org.';
104
+ // ---------------------------------------------------------------------------
105
+ // Shared response shapes
106
+ // ---------------------------------------------------------------------------
107
+ /**
108
+ * The failure envelope every route emits (`middleware/errorHandler.ts`).
109
+ * One schema, referenced by every documented error status, so a client can
110
+ * write one error branch.
111
+ */
112
+ const ERROR_SCHEMA = {
113
+ type: 'object',
114
+ required: ['success', 'error'],
115
+ properties: {
116
+ success: { type: 'boolean', enum: [false] },
117
+ error: { type: 'string', description: 'Human-readable failure reason.' },
118
+ details: {
119
+ description: 'Present on validation failures: the field-level issues that caused the 400.'
120
+ }
121
+ }
122
+ };
123
+ /** The `{ success: true, data }` envelope every successful route emits. */
124
+ function successSchema() {
125
+ return {
126
+ type: 'object',
127
+ required: ['success', 'data'],
128
+ properties: {
129
+ success: { type: 'boolean', enum: [true] },
130
+ data: { description: 'The operation result.' }
131
+ }
132
+ };
133
+ }
134
+ const ERROR_STATUSES = [
135
+ ['400', 'The request failed validation.'],
136
+ ['401', 'Missing, malformed, revoked or expired credential.'],
137
+ ['403', 'The key lacks the required scope, or the org is paused.'],
138
+ ['404', 'No such resource in the calling org (a cross-org id reads as absent).'],
139
+ ['409', 'Refused by a business rule — e.g. the recipient has opted out.'],
140
+ ['429', RATE_LIMIT_NOTE],
141
+ ['500', 'Unexpected server error.']
142
+ ];
143
+ function errorResponses(toolName) {
144
+ const out = {};
145
+ for (const [status, description] of ERROR_STATUSES) {
146
+ out[status] = {
147
+ description: status === '429' && TIGHTLY_LIMITED.has(toolName)
148
+ ? SEND_RATE_LIMIT_NOTE
149
+ : description,
150
+ content: {
151
+ 'application/json': {
152
+ schema: { $ref: '#/components/schemas/Error' }
153
+ }
154
+ }
155
+ };
156
+ }
157
+ return out;
158
+ }
159
+ // ---------------------------------------------------------------------------
160
+ // Webhook payloads
161
+ // ---------------------------------------------------------------------------
162
+ const ISO = { type: 'string', format: 'date-time' };
163
+ const NULLABLE_STRING = { type: ['string', 'null'] };
164
+ /**
165
+ * The `data` shape per event, mirroring `services/webhookEvents.ts` (the one
166
+ * place those payloads are built) and the flow/survey emitters.
167
+ */
168
+ const WEBHOOK_DATA_SCHEMAS = {
169
+ 'survey.completed': {
170
+ type: 'object',
171
+ properties: {
172
+ surveyId: { type: 'string' },
173
+ respondentNumber: { type: 'string', description: 'E.164.' },
174
+ answers: { type: 'object', additionalProperties: true },
175
+ completedAt: ISO
176
+ }
177
+ },
178
+ 'response.received': {
179
+ type: 'object',
180
+ properties: {
181
+ surveyId: { type: 'string' },
182
+ respondentNumber: { type: 'string', description: 'E.164.' },
183
+ questionId: { type: 'string' },
184
+ questionIndex: { type: 'integer' },
185
+ answer: {},
186
+ complete: { type: 'boolean' }
187
+ }
188
+ },
189
+ 'message.received': {
190
+ type: 'object',
191
+ properties: {
192
+ number: { type: 'string', description: 'E.164.' },
193
+ body: NULLABLE_STRING,
194
+ postbackData: {
195
+ ...NULLABLE_STRING,
196
+ description: 'Set when the contact tapped a button rather than typing.'
197
+ },
198
+ conversationId: NULLABLE_STRING,
199
+ occurredAt: ISO
200
+ }
201
+ },
202
+ 'message.delivered': {
203
+ type: 'object',
204
+ properties: {
205
+ number: { type: 'string', description: 'E.164.' },
206
+ providerSid: NULLABLE_STRING,
207
+ broadcastId: NULLABLE_STRING,
208
+ status: { type: 'string', enum: ['DELIVERED', 'READ'] },
209
+ channel: NULLABLE_STRING,
210
+ fellBackToSms: { type: 'boolean' },
211
+ occurredAt: ISO,
212
+ source: { type: 'string', enum: ['callback', 'reconcile'] }
213
+ }
214
+ },
215
+ 'message.failed': {
216
+ type: 'object',
217
+ properties: {
218
+ number: { type: 'string', description: 'E.164.' },
219
+ providerSid: NULLABLE_STRING,
220
+ broadcastId: NULLABLE_STRING,
221
+ errorCode: {
222
+ ...NULLABLE_STRING,
223
+ description: "The PROVIDER's own code, unmodified (e.g. 63049)."
224
+ },
225
+ errorMessage: NULLABLE_STRING,
226
+ channel: NULLABLE_STRING,
227
+ occurredAt: ISO,
228
+ source: { type: 'string', enum: ['callback', 'reconcile'] }
229
+ }
230
+ },
231
+ 'broadcast.completed': {
232
+ type: 'object',
233
+ properties: {
234
+ broadcastId: { type: 'string' },
235
+ name: NULLABLE_STRING,
236
+ channel: NULLABLE_STRING,
237
+ status: { type: 'string', enum: ['SENT', 'FAILED'] },
238
+ stats: {
239
+ type: 'object',
240
+ properties: {
241
+ total: { type: 'integer' },
242
+ delivered: { type: 'integer' },
243
+ read: { type: 'integer' },
244
+ failed: { type: 'integer' },
245
+ fellBackToSms: { type: 'integer' }
246
+ }
247
+ },
248
+ completedAt: ISO
249
+ }
250
+ },
251
+ 'contact.opted_out': {
252
+ type: 'object',
253
+ properties: {
254
+ number: { type: 'string', description: 'E.164.' },
255
+ body: {
256
+ ...NULLABLE_STRING,
257
+ description: 'What the contact actually sent, so the opt-out is auditable.'
258
+ },
259
+ occurredAt: ISO
260
+ }
261
+ },
262
+ 'action.tapped': {
263
+ type: 'object',
264
+ properties: {
265
+ number: { type: 'string', description: 'E.164.' },
266
+ postbackData: {
267
+ type: 'string',
268
+ description: 'The stable machine id the author set on the action.'
269
+ },
270
+ text: { ...NULLABLE_STRING, description: 'The chip label the recipient saw.' },
271
+ templateId: NULLABLE_STRING,
272
+ templateName: NULLABLE_STRING,
273
+ configuredUrl: {
274
+ ...NULLABLE_STRING,
275
+ description: 'The URL the author configured on the action. The platform never POSTs there itself — route on it from here.'
276
+ },
277
+ occurredAt: ISO
278
+ }
279
+ },
280
+ 'flow.started': {
281
+ type: 'object',
282
+ properties: {
283
+ runId: { type: 'string' },
284
+ flowId: { type: 'string' },
285
+ flowName: NULLABLE_STRING,
286
+ kind: { type: 'string', description: 'BOT or JOURNEY.' },
287
+ number: { type: 'string', description: 'E.164.' },
288
+ trigger: { type: 'string' },
289
+ startedAt: ISO
290
+ }
291
+ },
292
+ 'flow.completed': {
293
+ type: 'object',
294
+ properties: {
295
+ runId: { type: 'string' },
296
+ flowId: { type: 'string' },
297
+ number: { type: 'string', description: 'E.164.' },
298
+ status: { type: 'string' },
299
+ exitReason: { type: 'string' },
300
+ vars: {
301
+ type: 'object',
302
+ additionalProperties: true,
303
+ description: 'The variables the run collected.'
304
+ },
305
+ endedAt: ISO
306
+ }
307
+ },
308
+ 'contact.lead_captured': {
309
+ type: 'object',
310
+ properties: {
311
+ number: { type: 'string', description: 'E.164.' },
312
+ contactId: NULLABLE_STRING,
313
+ source: {
314
+ type: 'string',
315
+ description: 'The normalised source tag — what `Contact.attributes.leadSource` holds.'
316
+ },
317
+ origin: {
318
+ type: 'string',
319
+ enum: ['widget', 'webhook'],
320
+ description: 'How the lead reached us: a click-to-chat `ref:` inbound, or the lead webhook. The shape is identical either way.'
321
+ },
322
+ firstName: NULLABLE_STRING,
323
+ lastName: NULLABLE_STRING,
324
+ email: NULLABLE_STRING,
325
+ optInText: {
326
+ ...NULLABLE_STRING,
327
+ description: 'The wording the person agreed to — the legal artefact, verbatim.'
328
+ },
329
+ optInTextIsSystemDefault: {
330
+ type: 'boolean',
331
+ description: 'TRUE when `optInText` is wording the PLATFORM composed because the source supplied none — NOT wording the person was shown. Most integrations map no opt-in wording at all, so this is the common case: file a flagged sentence differently from captured consent.'
332
+ },
333
+ flowStarted: { type: 'boolean' },
334
+ welcomeSent: { type: 'boolean' },
335
+ occurredAt: ISO
336
+ }
337
+ }
338
+ };
339
+ /** One `webhooks` entry: the signed envelope + headers a receiver must verify. */
340
+ function webhookOperation(event) {
341
+ const data = WEBHOOK_DATA_SCHEMAS[event] ?? {
342
+ type: 'object',
343
+ additionalProperties: true,
344
+ description: 'Event payload. This event has no published shape yet — inspect a delivery.'
345
+ };
346
+ return {
347
+ post: {
348
+ operationId: `webhook_${event.replace(/[^A-Za-z0-9]+/g, '_')}`,
349
+ summary: `${event} event`,
350
+ description: `The platform POSTs this to every active endpoint subscribed to \`${event}\`. ` +
351
+ 'Verify `X-RCS-Signature` against the RAW body before trusting it. ' +
352
+ 'Respond 2xx to acknowledge; anything else is retried up to 5 attempts with a 1m/5m/25m/60m/60m backoff, and a receiver slower than 8s is aborted.',
353
+ parameters: [
354
+ {
355
+ name: 'X-RCS-Signature',
356
+ in: 'header',
357
+ required: true,
358
+ schema: { type: 'string' },
359
+ description: 't=<unixSeconds>,v1=<hex HMAC-SHA256 of "<t>.<rawBody>"> using the endpoint signing secret.'
360
+ },
361
+ {
362
+ name: 'X-RCS-Event',
363
+ in: 'header',
364
+ required: true,
365
+ schema: { type: 'string', enum: [event] },
366
+ description: 'The event type.'
367
+ },
368
+ {
369
+ name: 'X-RCS-Delivery',
370
+ in: 'header',
371
+ required: true,
372
+ schema: { type: 'string' },
373
+ description: 'Unique delivery id — use it as the idempotency key.'
374
+ }
375
+ ],
376
+ requestBody: {
377
+ required: true,
378
+ content: {
379
+ 'application/json': {
380
+ schema: {
381
+ type: 'object',
382
+ required: ['id', 'type', 'createdAt', 'data'],
383
+ properties: {
384
+ id: { type: 'string', description: 'Event id (matches X-RCS-Delivery).' },
385
+ type: { type: 'string', enum: [event] },
386
+ createdAt: ISO,
387
+ data
388
+ }
389
+ }
390
+ }
391
+ }
392
+ },
393
+ responses: {
394
+ '2XX': { description: 'Acknowledged. Anything else is retried.' }
395
+ }
396
+ }
397
+ };
398
+ }
399
+ // ---------------------------------------------------------------------------
400
+ // Operations
401
+ // ---------------------------------------------------------------------------
402
+ function parameterFor(name, schema, where, required) {
403
+ const { description, ...rest } = schema ?? {};
404
+ return {
405
+ name,
406
+ in: where,
407
+ required,
408
+ ...(description ? { description } : {}),
409
+ schema: rest
410
+ };
411
+ }
412
+ function operationFor(tool) {
413
+ const params = new Set(pathParamsOf(tool.path));
414
+ const properties = tool.inputSchema.properties ?? {};
415
+ const required = new Set(tool.inputSchema.required ?? []);
416
+ const hasBody = tool.method === 'POST' || tool.method === 'PUT' || tool.method === 'PATCH';
417
+ const parameters = [];
418
+ const bodyProps = {};
419
+ const bodyRequired = [];
420
+ for (const [name, raw] of Object.entries(properties)) {
421
+ const schema = (raw ?? {});
422
+ if (params.has(name)) {
423
+ // A path parameter is required by construction — the URL cannot be built
424
+ // without it, whatever the inputSchema happens to say.
425
+ parameters.push(parameterFor(name, schema, 'path', true));
426
+ continue;
427
+ }
428
+ if (hasBody) {
429
+ bodyProps[name] = schema;
430
+ if (required.has(name))
431
+ bodyRequired.push(name);
432
+ }
433
+ else {
434
+ parameters.push(parameterFor(name, schema, 'query', required.has(name)));
435
+ }
436
+ }
437
+ const successStatus = String(tool.successStatus ?? 200);
438
+ const operation = {
439
+ operationId: tool.name,
440
+ tags: [tagFor(tool)],
441
+ summary: firstSentence(tool.description),
442
+ description: tool.description,
443
+ ...(parameters.length ? { parameters } : {}),
444
+ responses: {
445
+ [successStatus]: {
446
+ description: 'Success.',
447
+ content: { 'application/json': { schema: successSchema() } }
448
+ },
449
+ ...errorResponses(tool.name)
450
+ }
451
+ };
452
+ // `(none)` is the scope string the catalog uses for the discovery endpoints.
453
+ // An empty `security` array is OpenAPI's way of saying "no requirement", and
454
+ // is the honest rendering of a route with no `requireScope`.
455
+ operation.security =
456
+ tool.scope === '(none)'
457
+ ? []
458
+ : [{ bearerAuth: [tool.scope] }, { oauth2: [tool.scope] }];
459
+ if (hasBody && Object.keys(bodyProps).length > 0) {
460
+ const schemaName = pascal(tool.name);
461
+ operation.requestBody = {
462
+ required: bodyRequired.length > 0,
463
+ content: {
464
+ 'application/json': {
465
+ schema: { $ref: `#/components/schemas/${schemaName}` }
466
+ }
467
+ }
468
+ };
469
+ operation['x-request-schema'] = schemaName;
470
+ }
471
+ return operation;
472
+ }
473
+ /**
474
+ * The component schema for a tool's request body — its `inputSchema` minus the
475
+ * properties that are carried in the URL. GET/DELETE tools get none: their
476
+ * whole input expands into parameters, so a component would be dead weight.
477
+ */
478
+ function requestSchemaFor(tool) {
479
+ const hasBody = tool.method === 'POST' || tool.method === 'PUT' || tool.method === 'PATCH';
480
+ if (!hasBody)
481
+ return null;
482
+ const params = new Set(pathParamsOf(tool.path));
483
+ const properties = {};
484
+ for (const [name, schema] of Object.entries(tool.inputSchema.properties ?? {})) {
485
+ if (!params.has(name))
486
+ properties[name] = schema;
487
+ }
488
+ if (Object.keys(properties).length === 0)
489
+ return null;
490
+ const required = (tool.inputSchema.required ?? []).filter((name) => !params.has(name));
491
+ return {
492
+ type: 'object',
493
+ properties,
494
+ ...(required.length ? { required } : {}),
495
+ ...(tool.inputSchema.additionalProperties === false
496
+ ? { additionalProperties: false }
497
+ : {})
498
+ };
499
+ }
500
+ // ---------------------------------------------------------------------------
501
+ // The document
502
+ // ---------------------------------------------------------------------------
503
+ /**
504
+ * Build the gateway's OpenAPI 3.1 document.
505
+ *
506
+ * `servers` is supplied by the caller because only the server knows which
507
+ * environment it is: a published spec whose host is a placeholder cannot be
508
+ * imported into Postman or a code generator without hand-editing, which is the
509
+ * exact defect this replaces.
510
+ */
511
+ function buildOpenApiDocument(servers, options = {}) {
512
+ const events = options.webhookEvents ?? tools_1.WEBHOOK_EVENT_NAMES;
513
+ const paths = {};
514
+ const schemas = { Error: ERROR_SCHEMA };
515
+ for (const tool of tools_1.TOOLS) {
516
+ const url = toOpenApiPath(tool.path);
517
+ const item = (paths[url] ??= {});
518
+ const verb = tool.method.toLowerCase();
519
+ if (item[verb]) {
520
+ throw new Error(`Two tools claim ${tool.method} ${tool.path} (${item[verb].operationId} and ${tool.name})`);
521
+ }
522
+ item[verb] = operationFor(tool);
523
+ const requestSchema = requestSchemaFor(tool);
524
+ if (requestSchema)
525
+ schemas[pascal(tool.name)] = requestSchema;
526
+ }
527
+ const webhooks = {};
528
+ for (const event of events)
529
+ webhooks[event] = webhookOperation(event);
530
+ return {
531
+ openapi: '3.1.0',
532
+ info: {
533
+ title: 'RCS Campaigns agent gateway',
534
+ version: options.version ?? '1.0.0',
535
+ description: 'The machine-facing API behind the MCP server, the CLI and the skill chains. ' +
536
+ 'Authenticate with a scoped `rcsk_live_…` agent key, or an OAuth 2.1 access token (`rcso_at_…`) from the remote MCP server. ' +
537
+ 'Every response is the envelope `{ success: true, data }` or `{ success: false, error }`. ' +
538
+ 'Generated from the shared tool catalog — the same list the MCP server and CLI run on — so it cannot drift from the implementation.',
539
+ license: { name: 'Proprietary', identifier: 'LicenseRef-Proprietary' }
540
+ },
541
+ servers,
542
+ // The default requirement; each operation restates its own scope, and the
543
+ // discovery endpoints override it with an empty array.
544
+ security: [{ bearerAuth: [] }, { oauth2: [] }],
545
+ tags: [
546
+ { name: 'Discovery', description: 'What this key may do, and this document.' },
547
+ { name: 'Templates', description: 'Author, render, submit and clone templates.' },
548
+ { name: 'Surveys', description: 'Author, submit, send and read conversational surveys.' },
549
+ { name: 'Messages', description: 'Send an approved template to a list of numbers.' },
550
+ { name: 'Conversations', description: 'Read threads, reply into them, hand them over.' },
551
+ { name: 'Contacts', description: 'The org-level person record behind every audience.' },
552
+ { name: 'Audiences', description: 'Saved contact lists.' },
553
+ { name: 'Webhooks', description: 'Subscribe to signed platform events.' },
554
+ { name: 'Media', description: 'Host an image on the platform bucket.' },
555
+ { name: 'Usage', description: 'Month-to-date metered usage.' }
556
+ ],
557
+ paths,
558
+ webhooks,
559
+ components: {
560
+ securitySchemes: {
561
+ bearerAuth: {
562
+ type: 'http',
563
+ scheme: 'bearer',
564
+ description: 'A scoped agent key (`rcsk_live_…`), minted at Settings → API keys and shown exactly once. Send it as `Authorization: Bearer …`.'
565
+ },
566
+ oauth2: {
567
+ type: 'oauth2',
568
+ description: 'The remote MCP server is an OAuth 2.1 authorization server with PKCE and Dynamic Client Registration. Access tokens look like `rcso_at_…` and resolve to the same org + scopes as a key.',
569
+ flows: {
570
+ authorizationCode: {
571
+ authorizationUrl: 'https://mcp.rcs-templates.com/oauth/authorize',
572
+ tokenUrl: 'https://mcp.rcs-templates.com/oauth/token',
573
+ refreshUrl: 'https://mcp.rcs-templates.com/oauth/token',
574
+ scopes: Object.fromEntries([...new Set(tools_1.TOOLS.map((t) => t.scope))]
575
+ .filter((scope) => scope !== '(none)')
576
+ .sort()
577
+ .map((scope) => [scope, scopeSummary(scope)]))
578
+ }
579
+ }
580
+ }
581
+ },
582
+ schemas
583
+ }
584
+ };
585
+ }
586
+ /** One line per scope for the OAuth flow's `scopes` map. */
587
+ function scopeSummary(scope) {
588
+ const names = tools_1.TOOLS.filter((t) => t.scope === scope).map((t) => t.name);
589
+ return names.join(', ');
590
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * The shared agent tool catalog.
3
+ *
4
+ * One declarative list of the gateway's capabilities — name, description, the
5
+ * JSON-Schema for inputs (used verbatim as the MCP `inputSchema`), and a pure
6
+ * function that turns validated args into a gateway RequestSpec. Both the MCP
7
+ * server (tools/list + tools/call) and the CLI build on this list, so adding a
8
+ * capability means editing ONE place and both skins gain it.
9
+ */
10
+ import { type Channel, type RequestSpec } from './client';
11
+ /** The HTTP verbs the gateway routes are registered under. */
12
+ export type ToolMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
13
+ export interface ToolDef {
14
+ name: string;
15
+ description: string;
16
+ /** The gateway scope a key needs for this tool (informational/discovery). */
17
+ scope: string;
18
+ /**
19
+ * STATIC route metadata — the verb and the Express-param path this tool calls
20
+ * (`/conversations/:id`), relative to the gateway base.
21
+ *
22
+ * `toRequest` below builds a CONCRETE request for concrete args, so it can
23
+ * never answer "what shape is this endpoint?" without being invoked. These
24
+ * two fields can, which is what lets ONE catalog back the MCP server, the
25
+ * CLI, `GET /api/agent/capabilities`, the generated OpenAPI document, and the
26
+ * route-coverage test that fails CI when a gateway route has no entry here.
27
+ *
28
+ * The invariant the OpenAPI generator depends on, pinned by the toolkit's own
29
+ * tests: every `:param` in `path` is also a property of `inputSchema`.
30
+ */
31
+ method: ToolMethod;
32
+ path: string;
33
+ /**
34
+ * The status the route returns on success — 200 unless the call CREATES a
35
+ * row, which returns 201. Static for the same reason `method`/`path` are:
36
+ * the generated OpenAPI document has to name a success code, and guessing
37
+ * "POST ⇒ 201" would be wrong for `submit_template`, `send_survey` and every
38
+ * other POST-as-action on this gateway.
39
+ */
40
+ successStatus?: 200 | 201;
41
+ /** JSON Schema for the tool arguments (MCP inputSchema, draft-07 style). */
42
+ inputSchema: {
43
+ type: 'object';
44
+ properties: Record<string, unknown>;
45
+ required?: string[];
46
+ additionalProperties?: boolean;
47
+ };
48
+ /** Build the HTTP request for these (already MCP-validated) args. */
49
+ toRequest: (args: Record<string, any>) => RequestSpec;
50
+ }
51
+ /**
52
+ * The outbound webhook event types an endpoint may subscribe to — THE list,
53
+ * not a copy of one.
54
+ *
55
+ * It lives here because this is the package both ends can reach: the toolkit
56
+ * is a standalone npm dependency and cannot import from `apps/server`, but
57
+ * `apps/server` already depends on the toolkit (`workspace:*`) for `TOOLS` and
58
+ * the OpenAPI generator, so the arrow only points one way. `services/webhooks.ts`
59
+ * re-exports this array as `WEBHOOK_EVENTS`; there is no second array to keep
60
+ * in step, and **adding an event means editing this one list**.
61
+ *
62
+ * It used to be hand-listed here and hand-listed again in the server, kept
63
+ * honest by an exact-equality test across the two packages. That test could
64
+ * only fail AFTER a merge — a branch adding an eleventh event to one list and
65
+ * a branch adding the enum to the other are each green alone and red together,
66
+ * and `deploy.yml` runs the server suite before deploying, so the desync would
67
+ * have blocked the backend deploy rather than merely CI. Deriving removes the
68
+ * failure mode instead of detecting it later.
69
+ *
70
+ * The values are persisted on `WebhookEndpoint.events`, so a name here is a
71
+ * contract with every endpoint already registered: ADD, never rename.
72
+ *
73
+ * `contact.lead_captured` (MSG-1832) is the eleventh, appended never renamed.
74
+ */
75
+ export declare const WEBHOOK_EVENT_NAMES: readonly ["survey.completed", "response.received", "message.received", "message.delivered", "message.failed", "broadcast.completed", "contact.opted_out", "action.tapped", "flow.started", "flow.completed", "payment.completed", "payment.failed", "contact.lead_captured"];
76
+ export declare const TOOLS: ToolDef[];
77
+ /** Look up a tool by name. */
78
+ export declare function findTool(name: string): ToolDef | undefined;
79
+ /** Does this tool accept a `channel` argument at all? */
80
+ export declare function acceptsChannel(tool: ToolDef): boolean;
81
+ /**
82
+ * Fill in the skin's default channel for a call that accepts one and did not
83
+ * specify it.
84
+ *
85
+ * Both skins run the same catalog against the same gateway, so the only thing
86
+ * that should differ is which channel you get when you don't ask: the `wa` bins
87
+ * assume WhatsApp, the `rcs` bins stay silent and let the server fall back to
88
+ * the org's own brand channel. An explicit `channel` from the caller always
89
+ * wins, and tools that take no channel are returned untouched — so this can be
90
+ * applied unconditionally at the CLI/MCP boundary.
91
+ */
92
+ export declare function withDefaultChannel(tool: ToolDef, args: Record<string, any>, defaultChannel?: Channel): Record<string, any>;
93
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +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,EA4uC1B,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"}