@ararahq/mcp 5.0.0 → 6.0.0

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,229 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { z } from "zod";
3
+ import { adminRequest } from "../lib/admin.js";
4
+ import { apiRequest } from "../lib/api.js";
5
+ import { assertOperatorAllowed } from "../lib/operator-access.js";
6
+ import { jsonValueSchema, pagedTemplatesSchema } from "../lib/schemas.js";
7
+ import { execute } from "../mcp/result.js";
8
+ import { destructive, idempotentWrite, readOnly, register as registerSharedTool, write, } from "./register.js";
9
+ const idSchema = z.string().uuid();
10
+ const amountSchema = z.number().positive().max(1_000_000);
11
+ /**
12
+ * Listing never exposes key material: only fields on this allowlist survive,
13
+ * regardless of what the backend returns.
14
+ */
15
+ export const safeApiKeySchema = z
16
+ .object({
17
+ id: z.string(),
18
+ name: z.string().optional(),
19
+ prefix: z.string().optional(),
20
+ last4: z.string().optional(),
21
+ maskedKey: z.string().optional(),
22
+ scope: z.string().optional(),
23
+ mode: z.string().optional(),
24
+ createdAt: z.string().optional(),
25
+ expiresAt: z.string().nullable().optional(),
26
+ })
27
+ .strip();
28
+ const walletMutationSchema = z
29
+ .object({ status: z.string(), organizationId: z.string(), amount: z.string() })
30
+ .passthrough();
31
+ const balanceSchema = z
32
+ .object({ organizationId: z.string(), balance: z.union([z.number(), z.string()]) })
33
+ .passthrough();
34
+ const failureBreakdownSchema = z
35
+ .object({
36
+ windowHours: z.number(),
37
+ totalMessages: z.number(),
38
+ totalFailures: z.number(),
39
+ byReason: z.record(z.number()),
40
+ byTemplate: z.record(z.number()),
41
+ topCodes: z.record(z.number()),
42
+ })
43
+ .passthrough();
44
+ const anyRecordSchema = z.record(jsonValueSchema);
45
+ export const filterTemplates = (templates, filter) => {
46
+ if (filter === undefined || filter.length === 0)
47
+ return templates;
48
+ const needle = filter.toLowerCase();
49
+ return templates.filter((template) => template.name.toLowerCase().includes(needle) ||
50
+ template.providerStatus.toLowerCase().includes(needle) ||
51
+ template.category.toLowerCase().includes(needle));
52
+ };
53
+ /** Every operator tool runs behind the e-mail allowlist gate (fail closed). */
54
+ const register = (server, name, description, inputSchema, annotations, handler) => {
55
+ registerSharedTool(server, name, description, inputSchema, annotations, handler, {
56
+ gate: () => assertOperatorAllowed(),
57
+ });
58
+ };
59
+ export const OPERATOR_TOOL_NAMES = [
60
+ "create_api_key",
61
+ "list_api_keys",
62
+ "revoke_api_key",
63
+ "configure_webhook_route",
64
+ "list_templates",
65
+ "delete_template",
66
+ "template_health",
67
+ "list_failures",
68
+ "get_balance",
69
+ "add_credit",
70
+ "remove_credit",
71
+ "list_transactions",
72
+ ];
73
+ export const registerOperatorTools = (server) => {
74
+ register(server, "create_api_key", "Create an API key. The key value is returned ONCE in this response and can never be listed again.", {
75
+ name: z.string().trim().min(1).max(255),
76
+ mode: z.enum(["LIVE", "TEST"]).default("LIVE"),
77
+ scope: z.enum(["ADMIN", "SEND_ONLY"]).default("ADMIN"),
78
+ }, write, async (input) => execute(async () => ({
79
+ data: await apiRequest("/v1/api-keys", {
80
+ method: "POST",
81
+ body: { name: input.name, mode: input.mode, scope: input.scope },
82
+ schema: anyRecordSchema,
83
+ retry: false,
84
+ }),
85
+ message: "API key created. Store the key value now: it will never be shown again.",
86
+ })));
87
+ register(server, "list_api_keys", "List API keys (prefix, last4 and scope only — never the key value).", {}, readOnly, async () => execute(async () => {
88
+ const keys = await apiRequest("/v1/api-keys", { schema: z.array(safeApiKeySchema) });
89
+ return { data: keys, message: `${keys.length} API key(s) loaded.` };
90
+ }));
91
+ register(server, "revoke_api_key", "Revoke an API key permanently.", { apiKeyId: idSchema }, destructive, async (input) => execute(async () => {
92
+ const apiKeyId = idSchema.parse(input.apiKeyId);
93
+ await apiRequest(`/v1/api-keys/${apiKeyId}`, {
94
+ method: "DELETE",
95
+ schema: z.unknown(),
96
+ });
97
+ return { data: { apiKeyId, revoked: true }, message: "API key revoked." };
98
+ }));
99
+ register(server, "configure_webhook_route", "Configure the A/B template pool for an API key's webhook send route.", {
100
+ apiKeyId: idSchema,
101
+ senderNumberId: idSchema,
102
+ templateIds: z.array(idSchema).min(1).max(20),
103
+ secondarySenderNumberId: idSchema.optional(),
104
+ }, idempotentWrite, async (input) => execute(async () => {
105
+ const apiKeyId = idSchema.parse(input.apiKeyId);
106
+ return {
107
+ data: await apiRequest(`/v1/api-keys/${apiKeyId}/webhook-route`, {
108
+ method: "PUT",
109
+ body: {
110
+ senderNumberId: input.senderNumberId,
111
+ templateIds: input.templateIds,
112
+ ...(input.secondarySenderNumberId === undefined
113
+ ? {}
114
+ : { secondarySenderNumberId: input.secondarySenderNumberId }),
115
+ },
116
+ schema: anyRecordSchema,
117
+ }),
118
+ message: "Webhook route configured.",
119
+ };
120
+ }));
121
+ register(server, "list_templates", "List templates with name, status, category and availability. Optional filter matches name, status or category.", { filter: z.string().max(100).optional() }, readOnly, async (input) => execute(async () => {
122
+ const templates = await apiRequest("/v1/templates?size=100", {
123
+ schema: pagedTemplatesSchema,
124
+ });
125
+ const filter = typeof input.filter === "string" ? input.filter : undefined;
126
+ const data = filterTemplates(templates, filter);
127
+ return { data, message: `${data.length} template(s) loaded.` };
128
+ }));
129
+ register(server, "delete_template", "Delete a template.", { templateId: idSchema }, destructive, async (input) => execute(async () => {
130
+ const templateId = idSchema.parse(input.templateId);
131
+ await apiRequest(`/v1/templates/${templateId}`, {
132
+ method: "DELETE",
133
+ schema: z.unknown(),
134
+ });
135
+ return { data: { templateId, deleted: true }, message: "Template deleted." };
136
+ }));
137
+ register(server, "template_health", "Delivery, read and failure analytics for one template.", { templateId: idSchema }, readOnly, async (input) => execute(async () => {
138
+ const templateId = idSchema.parse(input.templateId);
139
+ return {
140
+ data: await apiRequest(`/v1/templates/${templateId}/analytics`, {
141
+ schema: anyRecordSchema,
142
+ }),
143
+ message: "Template health loaded.",
144
+ };
145
+ }));
146
+ register(server, "list_failures", "Admin: breakdown of send failures by reason (paused / recipient / media / other) in a time window.", { organizationId: idSchema, hours: z.number().int().min(1).max(720).default(24) }, readOnly, async (input) => execute(async () => {
147
+ const organizationId = idSchema.parse(input.organizationId);
148
+ const hours = z
149
+ .number()
150
+ .int()
151
+ .min(1)
152
+ .max(720)
153
+ .parse(input.hours ?? 24);
154
+ const data = await adminRequest(`/v1/admin/diagnostics/failures?organizationId=${organizationId}&hours=${hours}`, { schema: failureBreakdownSchema });
155
+ return {
156
+ data,
157
+ message: `${data.totalFailures} failure(s) in the last ${data.windowHours}h.`,
158
+ };
159
+ }));
160
+ register(server, "get_balance", "Admin: wallet balance for an organization.", { organizationId: idSchema }, readOnly, async (input) => execute(async () => {
161
+ const organizationId = idSchema.parse(input.organizationId);
162
+ const data = await adminRequest(`/v1/admin/wallet/${organizationId}/balance`, {
163
+ schema: balanceSchema,
164
+ });
165
+ return { data, message: `Balance: R$ ${String(data.balance)}.` };
166
+ }));
167
+ register(server, "add_credit", "Admin: add wallet credit through the ledger. Idempotent per externalId.", {
168
+ organizationId: idSchema,
169
+ amount: amountSchema,
170
+ description: z.string().trim().min(1).max(255),
171
+ externalId: z.string().trim().min(1).max(100).optional(),
172
+ }, idempotentWrite, async (input) => execute(async () => {
173
+ const externalId = typeof input.externalId === "string" ? input.externalId : `mcp-credit-${randomUUID()}`;
174
+ const data = await adminRequest("/v1/admin/wallet/credit", {
175
+ method: "POST",
176
+ body: {
177
+ organizationId: input.organizationId,
178
+ amount: input.amount,
179
+ description: input.description,
180
+ externalId,
181
+ },
182
+ schema: walletMutationSchema,
183
+ idempotencyKey: externalId,
184
+ });
185
+ return { data: { ...data, externalId }, message: "Credit applied through the ledger." };
186
+ }));
187
+ register(server, "remove_credit", "Admin: debit wallet credit through the ledger, with a mandatory reason. Idempotent per externalId. Never leaves a negative balance unless allowNegative is explicitly true.", {
188
+ organizationId: idSchema,
189
+ amount: amountSchema,
190
+ reason: z.string().trim().min(1).max(255),
191
+ externalId: z.string().trim().min(1).max(100).optional(),
192
+ allowNegative: z.boolean().default(false),
193
+ }, idempotentWrite, async (input) => execute(async () => {
194
+ const externalId = typeof input.externalId === "string" ? input.externalId : `mcp-debit-${randomUUID()}`;
195
+ const data = await adminRequest("/v1/admin/wallet/debit", {
196
+ method: "POST",
197
+ body: {
198
+ organizationId: input.organizationId,
199
+ amount: input.amount,
200
+ reason: input.reason,
201
+ externalId,
202
+ allowNegative: input.allowNegative === true,
203
+ },
204
+ schema: walletMutationSchema,
205
+ idempotencyKey: externalId,
206
+ });
207
+ return { data: { ...data, externalId }, message: "Debit applied through the ledger." };
208
+ }));
209
+ register(server, "list_transactions", "Wallet ledger entries for the authenticated organization, newest first.", {
210
+ page: z.number().int().nonnegative().default(0),
211
+ size: z.number().int().min(1).max(100).default(50),
212
+ }, readOnly, async (input) => execute(async () => {
213
+ const page = z
214
+ .number()
215
+ .int()
216
+ .nonnegative()
217
+ .parse(input.page ?? 0);
218
+ const size = z
219
+ .number()
220
+ .int()
221
+ .min(1)
222
+ .max(100)
223
+ .parse(input.size ?? 50);
224
+ const data = await apiRequest(`/v1/wallet/transactions?page=${page}&size=${size}`, {
225
+ schema: anyRecordSchema,
226
+ });
227
+ return { data, message: "Wallet transactions loaded." };
228
+ }));
229
+ };
@@ -0,0 +1,46 @@
1
+ import { failure, toolOutputSchema } from "../mcp/result.js";
2
+ export const readOnly = {
3
+ readOnlyHint: true,
4
+ destructiveHint: false,
5
+ idempotentHint: true,
6
+ openWorldHint: true,
7
+ };
8
+ export const write = {
9
+ readOnlyHint: false,
10
+ destructiveHint: false,
11
+ idempotentHint: false,
12
+ openWorldHint: true,
13
+ };
14
+ export const idempotentWrite = { ...write, idempotentHint: true };
15
+ export const destructive = {
16
+ readOnlyHint: false,
17
+ destructiveHint: true,
18
+ idempotentHint: true,
19
+ openWorldHint: true,
20
+ };
21
+ /**
22
+ * Registers a tool with the shared output envelope. An optional [gate] runs before
23
+ * the handler and refuses the call through the same structured failure envelope. An
24
+ * optional [ui] resource URI links the tool to an MCP Apps panel.
25
+ */
26
+ export const register = (server, name, description, inputSchema, annotations, handler, options = {}) => {
27
+ const { gate, ui } = options;
28
+ const wrapped = gate === undefined
29
+ ? handler
30
+ : async (input) => {
31
+ try {
32
+ await gate();
33
+ }
34
+ catch (error) {
35
+ return failure(error);
36
+ }
37
+ return handler(input);
38
+ };
39
+ server.registerTool(name, {
40
+ description,
41
+ inputSchema,
42
+ outputSchema: toolOutputSchema,
43
+ annotations,
44
+ ...(ui === undefined ? {} : { _meta: { ui: { resourceUri: ui } } }),
45
+ }, wrapped);
46
+ };
@@ -0,0 +1,150 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { z } from "zod";
3
+ import { apiRequest } from "../lib/api.js";
4
+ import { AraraError, toAraraError } from "../lib/errors.js";
5
+ import { E164_PATTERN, recipientLabel, resolveRecipient } from "../lib/recipients.js";
6
+ import { messageSchema, pagedTemplatesSchema, templateStatusSchema } from "../lib/schemas.js";
7
+ import { windowStatusSchema } from "../lib/schemas.js";
8
+ import { execute } from "../mcp/result.js";
9
+ import { uiResourceUri } from "../ui/resources.js";
10
+ import { readOnly, register, write } from "./register.js";
11
+ const WINDOW_CLOSED_CODE = "CONVERSATION_WINDOW_CLOSED";
12
+ const APPROVED_STATUS = "APPROVED";
13
+ const MAX_MESSAGE_LENGTH = 4096;
14
+ const MAX_VARIABLES = 20;
15
+ const MAX_VARIABLE_LENGTH = 1024;
16
+ const MAX_TEMPLATE_SUGGESTIONS = 10;
17
+ export const e164Schema = z.string().regex(E164_PATTERN, "Use E.164, for example +5511999999999.");
18
+ export const recipientInputSchema = z.string().trim().min(1).max(255);
19
+ export const variablesSchema = z.array(z.string().max(MAX_VARIABLE_LENGTH)).max(MAX_VARIABLES);
20
+ const idSchema = z.string().uuid();
21
+ export const loadApprovedTemplateNames = async () => {
22
+ const params = new URLSearchParams({ status: APPROVED_STATUS, size: "100" });
23
+ const templates = await apiRequest(`/v1/templates?${params.toString()}`, {
24
+ schema: pagedTemplatesSchema,
25
+ });
26
+ return templates
27
+ .filter((template) => template.availableForSending)
28
+ .map((template) => template.name);
29
+ };
30
+ const windowClosedError = async (label) => {
31
+ const approved = await loadApprovedTemplateNames().catch(() => []);
32
+ const hint = approved.length === 0
33
+ ? "There is no approved template yet. Create one with create_template."
34
+ : `Approved templates: ${approved.slice(0, MAX_TEMPLATE_SUGGESTIONS).join(", ")}. Send one with send_whatsapp using templateName.`;
35
+ return new AraraError(WINDOW_CLOSED_CODE, `The 24h window with ${label} is closed, so free text is not allowed. ${hint}`, 422, false);
36
+ };
37
+ const buildSendBody = (input) => {
38
+ const base = input.from === undefined ? {} : { sender: input.from };
39
+ if (input.templateName !== undefined) {
40
+ return {
41
+ ...base,
42
+ receiver: input.phone,
43
+ type: "template",
44
+ templateName: input.templateName,
45
+ variables: input.variables,
46
+ };
47
+ }
48
+ return { ...base, receiver: input.phone, type: "text", body: input.message };
49
+ };
50
+ const sendOne = async (input, label) => {
51
+ try {
52
+ return await apiRequest("/v1/messages", {
53
+ method: "POST",
54
+ body: buildSendBody(input),
55
+ schema: messageSchema,
56
+ idempotencyKey: input.idempotencyKey,
57
+ });
58
+ }
59
+ catch (error) {
60
+ const normalized = toAraraError(error);
61
+ if (normalized.code === WINDOW_CLOSED_CODE && input.templateName === undefined) {
62
+ throw await windowClosedError(label);
63
+ }
64
+ throw normalized;
65
+ }
66
+ };
67
+ const registerSendWhatsapp = (server) => {
68
+ register(server, "send_whatsapp", "Send one WhatsApp message to one person. Pass 'to' as a phone in any format or a saved contact name. Pass 'message' for free text (only works inside the 24h window) or 'templateName' + 'variables' for an approved template (works any time). If the window is closed the error lists your approved templates. For many people use broadcast.", {
69
+ to: recipientInputSchema,
70
+ message: z.string().trim().min(1).max(MAX_MESSAGE_LENGTH).optional(),
71
+ templateName: z.string().trim().min(1).optional(),
72
+ variables: variablesSchema.default([]),
73
+ from: e164Schema.optional(),
74
+ idempotencyKey: z.string().uuid().optional(),
75
+ }, write, async (input) => execute(async () => {
76
+ const message = typeof input.message === "string" ? input.message : undefined;
77
+ const templateName = typeof input.templateName === "string" ? input.templateName : undefined;
78
+ if ((message === undefined) === (templateName === undefined)) {
79
+ throw new AraraError("INVALID_INPUT", "Pass exactly one of 'message' (free text) or 'templateName' (approved template).", 400, false);
80
+ }
81
+ const recipient = await resolveRecipient(recipientInputSchema.parse(input.to));
82
+ const label = recipientLabel(recipient);
83
+ const idempotencyKey = typeof input.idempotencyKey === "string" ? input.idempotencyKey : randomUUID();
84
+ const data = await sendOne({
85
+ phone: recipient.phone,
86
+ variables: variablesSchema.parse(input.variables),
87
+ idempotencyKey,
88
+ ...(message === undefined ? {} : { message }),
89
+ ...(templateName === undefined ? {} : { templateName }),
90
+ ...(typeof input.from === "string" ? { from: input.from } : {}),
91
+ }, label);
92
+ return {
93
+ data: { ...data, recipient, idempotencyKey },
94
+ message: `Message to ${label} accepted (${data.status}). Check delivery with check_status.`,
95
+ };
96
+ }));
97
+ };
98
+ const checkMessage = async (messageId) => {
99
+ const data = await apiRequest(`/v1/messages/${encodeURIComponent(messageId)}`, {
100
+ schema: messageSchema,
101
+ });
102
+ return { data, message: `Message ${messageId}: ${data.status}.` };
103
+ };
104
+ const checkTemplate = async (templateId) => {
105
+ const data = await apiRequest(`/v1/templates/${templateId}/status`, {
106
+ schema: templateStatusSchema,
107
+ });
108
+ const reason = typeof data.rejectionReason === "string" && data.rejectionReason.length > 0
109
+ ? ` Reason: ${data.rejectionReason}`
110
+ : "";
111
+ return { data, message: `Template ${templateId}: ${data.status}.${reason}` };
112
+ };
113
+ const checkWindow = async (to) => {
114
+ const recipient = await resolveRecipient(to);
115
+ const result = await apiRequest("/v1/conversations/window-status", {
116
+ method: "POST",
117
+ body: { phones: [recipient.phone] },
118
+ schema: windowStatusSchema,
119
+ retry: false,
120
+ });
121
+ const [status] = result.results;
122
+ const isWindowOpen = status?.isWindowOpen === true;
123
+ const hours = typeof status?.hoursRemaining === "number" ? `${status.hoursRemaining.toFixed(1)}h left` : "";
124
+ const label = recipientLabel(recipient);
125
+ const message = isWindowOpen
126
+ ? `${label}: window OPEN (${hours}). Free text is allowed.`
127
+ : `${label}: window CLOSED. Only an approved template can be sent.`;
128
+ return { data: { recipient, isWindowOpen, ...status }, message };
129
+ };
130
+ const registerCheckStatus = (server) => {
131
+ register(server, "check_status", "Answer 'did it arrive?', 'can I text them now?' and 'was my template approved?'. Pass exactly one of: 'to' (phone or contact name, returns whether the 24h window is open), 'messageId' (the id returned by send_whatsapp; delivery status and cost) or 'templateId' (Meta approval status with the rejection reason).", {
132
+ to: recipientInputSchema.optional(),
133
+ messageId: z.string().trim().min(1).optional(),
134
+ templateId: idSchema.optional(),
135
+ }, readOnly, async (input) => execute(async () => {
136
+ const provided = ["to", "messageId", "templateId"].filter((key) => typeof input[key] === "string");
137
+ if (provided.length !== 1) {
138
+ throw new AraraError("INVALID_INPUT", "Pass exactly one of 'to', 'messageId' or 'templateId'.", 400, false);
139
+ }
140
+ if (typeof input.messageId === "string")
141
+ return checkMessage(input.messageId);
142
+ if (typeof input.templateId === "string")
143
+ return checkTemplate(idSchema.parse(input.templateId));
144
+ return checkWindow(recipientInputSchema.parse(input.to));
145
+ }), { ui: uiResourceUri("status") });
146
+ };
147
+ export const registerSendTools = (server) => {
148
+ registerSendWhatsapp(server);
149
+ registerCheckStatus(server);
150
+ };
@@ -0,0 +1,63 @@
1
+ import { z } from "zod";
2
+ import { apiRequest } from "../lib/api.js";
3
+ import { templateSchema } from "../lib/schemas.js";
4
+ import { execute } from "../mcp/result.js";
5
+ import { register, write } from "./register.js";
6
+ import { e164Schema } from "./send.js";
7
+ const MAX_TEMPLATE_NAME = 512;
8
+ const MAX_TEMPLATE_BODY = 4096;
9
+ const MAX_FOOTER = 60;
10
+ const MAX_BUTTONS = 2;
11
+ const DEFAULT_LANGUAGE = "pt_BR";
12
+ const DEFAULT_HEADER_TYPE = "text";
13
+ const buttonSchema = z.object({
14
+ type: z.enum(["QUICK_REPLY", "URL", "PHONE_NUMBER", "SMART_LINK", "COPY_CODE"]),
15
+ text: z.string().trim().min(1),
16
+ url: z.string().url().optional(),
17
+ phone: e164Schema.optional(),
18
+ });
19
+ const registerCreateTemplate = (server) => {
20
+ register(server, "create_template", "Submit a WhatsApp template for Meta approval. Needed for broadcasts and for messaging outside the 24h window. Body uses positional placeholders like 'Oi {{1}}, seu pedido {{2}} saiu'; Meta rejects bodies that are only variables. Up to 2 buttons; a SMART_LINK button gets its clicks counted in campaign_report. Track approval with check_status(templateId).", {
21
+ name: z
22
+ .string()
23
+ .regex(/^[a-z0-9_]+$/, "Lowercase letters, digits and underscores only.")
24
+ .max(MAX_TEMPLATE_NAME),
25
+ category: z.enum(["MARKETING", "UTILITY", "AUTHENTICATION"]),
26
+ body: z.string().trim().min(1).max(MAX_TEMPLATE_BODY),
27
+ language: z.string().default(DEFAULT_LANGUAGE),
28
+ header: z.string().trim().min(1).optional(),
29
+ headerType: z.enum(["text", "media", "document"]).optional(),
30
+ footer: z.string().trim().min(1).max(MAX_FOOTER).optional(),
31
+ samples: z.record(z.string()).optional(),
32
+ buttons: z.array(buttonSchema).max(MAX_BUTTONS).optional(),
33
+ }, write, async (input) => execute(async () => {
34
+ const header = typeof input.header === "string"
35
+ ? {
36
+ header: input.header,
37
+ headerType: typeof input.headerType === "string" ? input.headerType : DEFAULT_HEADER_TYPE,
38
+ }
39
+ : {};
40
+ const data = await apiRequest("/v1/templates", {
41
+ method: "POST",
42
+ body: {
43
+ name: input.name,
44
+ category: input.category,
45
+ body: input.body,
46
+ language: input.language,
47
+ ...header,
48
+ ...(typeof input.footer === "string" ? { footer: input.footer } : {}),
49
+ ...(input.samples === undefined ? {} : { samples: input.samples }),
50
+ ...(input.buttons === undefined ? {} : { buttons: input.buttons }),
51
+ },
52
+ schema: templateSchema,
53
+ retry: false,
54
+ });
55
+ return {
56
+ data,
57
+ message: `Template '${data.name}' submitted (${data.providerStatus}). Approval usually takes minutes; check with check_status(templateId: "${data.id}").`,
58
+ };
59
+ }));
60
+ };
61
+ export const registerTemplateTools = (server) => {
62
+ registerCreateTemplate(server);
63
+ };