@canonmsg/agent-tools 0.6.0 → 0.8.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.
package/README.md CHANGED
@@ -2,7 +2,13 @@
2
2
 
3
3
  Model-facing tool definitions for the canonical Canon verbs, plus the dispatch that executes them.
4
4
 
5
- This is the public tool surface of Canon's verb layer. All eighteen `canon.verbs.v1` verbs — `send_to`, `request_input`, `request_approval`, `check_approval`, `send_card`, `request_card`, `share_contact`, `react`, `forward`, `create_group`, `add_member`, `remove_member`, `leave_conversation`, `list_contacts`, `list_contact_requests`, `list_conversations`, `cancel_contact_request`, `no_reply` — are projected here as JSON-Schema tool definitions and dispatched over one endpoint: `POST /agent/verbs/:verb`.
5
+ This is the model-facing projection of Canon's verb layer. Everyday outbound
6
+ communication uses one `communicate` tool for existing messages, new direct
7
+ conversations, group creation, forwarding, contact sharing, and group
8
+ membership. Interaction, card, reaction, and `no_reply` verbs remain separate.
9
+ Contact and conversation reads stay developer APIs and are not automatically
10
+ projected beside `communicate`. Everything dispatches through the canonical
11
+ `POST /agent/verbs/:verb` endpoints.
6
12
 
7
13
  Use it if you are binding Canon into an LLM runtime that speaks tools (an MCP server, or an in-process tool mount). If you are writing an agent, use [`@canonmsg/agent-sdk`](https://www.npmjs.com/package/@canonmsg/agent-sdk) instead — it wraps this layer for you.
8
14
 
@@ -0,0 +1,20 @@
1
+ import { type CanonClient, type CommunicateInput } from '@canonmsg/core';
2
+ import type { VerbToolDefinition, VerbToolResult } from './verb-tools.js';
3
+ export declare const CANON_COMMUNICATE_TOOL_NAME = "communicate";
4
+ /**
5
+ * The complete model-facing communication surface. It is exported separately
6
+ * from the in-conversation verb list so each runtime can opt into it.
7
+ */
8
+ export declare function canonCommunicateToolDefinition(name?: string): VerbToolDefinition;
9
+ /** Runtime validation mirrors the strict schema for bindings that dispatch directly. */
10
+ export declare function parseCommunicateToolInput(value: unknown): CommunicateInput;
11
+ export declare function executeCanonCommunicateTool(client: CanonClient, args: unknown): Promise<VerbToolResult>;
12
+ export interface CanonCommunicationBinding {
13
+ tools: ReadonlyArray<VerbToolDefinition>;
14
+ isToolName(name: string): boolean;
15
+ execute(client: CanonClient, toolName: string, args: unknown): Promise<VerbToolResult>;
16
+ }
17
+ /** A one-tool binding that runtimes may mount without adding any other tools. */
18
+ export declare function createCanonCommunicationBinding(options?: {
19
+ toolName?: string;
20
+ }): CanonCommunicationBinding;
@@ -0,0 +1,346 @@
1
+ import { parseDirectConversationSelection, VERB_LIMITS, } from '@canonmsg/core';
2
+ export const CANON_COMMUNICATE_TOOL_NAME = 'communicate';
3
+ const NON_EMPTY_ID = { type: 'string', minLength: 1, maxLength: 160 };
4
+ const MESSAGE_TEXT = { type: 'string', minLength: 1, maxLength: 4096 };
5
+ const OPTIONAL_CAPTION = { type: 'string', maxLength: 4096 };
6
+ const DIRECT_SELECTION_SCHEMA = {
7
+ oneOf: [
8
+ {
9
+ type: 'object',
10
+ required: ['mode'],
11
+ additionalProperties: false,
12
+ properties: { mode: { const: 'latest_or_new' } },
13
+ },
14
+ {
15
+ type: 'object',
16
+ required: ['mode'],
17
+ additionalProperties: false,
18
+ properties: { mode: { const: 'new' } },
19
+ },
20
+ {
21
+ type: 'object',
22
+ required: ['mode', 'conversationId'],
23
+ additionalProperties: false,
24
+ properties: {
25
+ mode: { const: 'specific' },
26
+ conversationId: NON_EMPTY_ID,
27
+ },
28
+ },
29
+ ],
30
+ };
31
+ /**
32
+ * The complete model-facing communication surface. It is exported separately
33
+ * from the in-conversation verb list so each runtime can opt into it.
34
+ */
35
+ export function canonCommunicateToolDefinition(name = CANON_COMMUNICATE_TOOL_NAME) {
36
+ return {
37
+ name,
38
+ description: 'Communicate in Canon. Message a known conversation, start/continue a '
39
+ + 'direct conversation, create a group, forward an exact message, share a '
40
+ + 'contact, or manage group members. Canon '
41
+ + 'enforces the recipient and agent-deployer policies; a new direct '
42
+ + 'conversation or group member may require approval.',
43
+ inputSchema: {
44
+ type: 'object',
45
+ oneOf: [
46
+ {
47
+ type: 'object',
48
+ required: ['action', 'conversationId', 'text'],
49
+ additionalProperties: false,
50
+ properties: {
51
+ action: { const: 'message_existing' },
52
+ conversationId: NON_EMPTY_ID,
53
+ text: MESSAGE_TEXT,
54
+ messageId: NON_EMPTY_ID,
55
+ },
56
+ },
57
+ {
58
+ type: 'object',
59
+ required: ['action', 'principalId', 'text'],
60
+ additionalProperties: false,
61
+ properties: {
62
+ action: { const: 'start_direct' },
63
+ principalId: NON_EMPTY_ID,
64
+ text: MESSAGE_TEXT,
65
+ selection: DIRECT_SELECTION_SCHEMA,
66
+ messageId: NON_EMPTY_ID,
67
+ },
68
+ },
69
+ {
70
+ type: 'object',
71
+ required: ['action', 'name', 'memberIds'],
72
+ additionalProperties: false,
73
+ properties: {
74
+ action: { const: 'create_group' },
75
+ name: { type: 'string', minLength: 1, maxLength: VERB_LIMITS.groupNameChars },
76
+ memberIds: {
77
+ type: 'array',
78
+ minItems: 1,
79
+ maxItems: VERB_LIMITS.groupMembers - 1,
80
+ uniqueItems: true,
81
+ items: NON_EMPTY_ID,
82
+ },
83
+ },
84
+ },
85
+ {
86
+ type: 'object',
87
+ required: [
88
+ 'action',
89
+ 'sourceConversationId',
90
+ 'targetConversationId',
91
+ 'messageId',
92
+ ],
93
+ additionalProperties: false,
94
+ properties: {
95
+ action: { const: 'forward_message' },
96
+ sourceConversationId: NON_EMPTY_ID,
97
+ targetConversationId: NON_EMPTY_ID,
98
+ messageId: NON_EMPTY_ID,
99
+ text: OPTIONAL_CAPTION,
100
+ },
101
+ },
102
+ {
103
+ type: 'object',
104
+ required: ['action', 'conversationId', 'contactUserId'],
105
+ additionalProperties: false,
106
+ properties: {
107
+ action: { const: 'share_contact' },
108
+ conversationId: NON_EMPTY_ID,
109
+ contactUserId: NON_EMPTY_ID,
110
+ text: OPTIONAL_CAPTION,
111
+ messageId: NON_EMPTY_ID,
112
+ },
113
+ },
114
+ {
115
+ type: 'object',
116
+ required: ['action', 'conversationId', 'userId', 'operation'],
117
+ additionalProperties: false,
118
+ properties: {
119
+ action: { const: 'manage_group_members' },
120
+ conversationId: NON_EMPTY_ID,
121
+ userId: NON_EMPTY_ID,
122
+ operation: { enum: ['add', 'remove'] },
123
+ },
124
+ },
125
+ ],
126
+ },
127
+ };
128
+ }
129
+ function isRecord(value) {
130
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
131
+ }
132
+ function readRequiredString(value) {
133
+ return typeof value === 'string' && value.trim() ? value : null;
134
+ }
135
+ function hasOnlyKeys(record, allowed) {
136
+ return Object.keys(record).every((key) => allowed.has(key));
137
+ }
138
+ /** Runtime validation mirrors the strict schema for bindings that dispatch directly. */
139
+ export function parseCommunicateToolInput(value) {
140
+ if (!isRecord(value))
141
+ throw new Error('communicate arguments must be an object');
142
+ if (value.action === 'create_group') {
143
+ if (!hasOnlyKeys(value, new Set(['action', 'name', 'memberIds']))) {
144
+ throw new Error('create_group contains unsupported fields');
145
+ }
146
+ const name = readRequiredString(value.name);
147
+ if (!name)
148
+ throw new Error('communicate.name is required');
149
+ if (!Array.isArray(value.memberIds)
150
+ || value.memberIds.length === 0
151
+ || !value.memberIds.every((entry) => readRequiredString(entry) !== null)) {
152
+ throw new Error('communicate.memberIds must contain at least one principal id');
153
+ }
154
+ return {
155
+ action: 'create_group',
156
+ name,
157
+ memberIds: [...new Set(value.memberIds)],
158
+ };
159
+ }
160
+ if (value.action === 'forward_message') {
161
+ if (!hasOnlyKeys(value, new Set([
162
+ 'action',
163
+ 'sourceConversationId',
164
+ 'targetConversationId',
165
+ 'messageId',
166
+ 'text',
167
+ ]))) {
168
+ throw new Error('forward_message contains unsupported fields');
169
+ }
170
+ const sourceConversationId = readRequiredString(value.sourceConversationId);
171
+ const targetConversationId = readRequiredString(value.targetConversationId);
172
+ const messageId = readRequiredString(value.messageId);
173
+ if (!sourceConversationId)
174
+ throw new Error('communicate.sourceConversationId is required');
175
+ if (!targetConversationId)
176
+ throw new Error('communicate.targetConversationId is required');
177
+ if (!messageId)
178
+ throw new Error('communicate.messageId is required');
179
+ if (value.text !== undefined && typeof value.text !== 'string') {
180
+ throw new Error('communicate.text must be a string');
181
+ }
182
+ return {
183
+ action: 'forward_message',
184
+ sourceConversationId,
185
+ targetConversationId,
186
+ messageId,
187
+ ...(value.text !== undefined ? { text: value.text } : {}),
188
+ };
189
+ }
190
+ if (value.action === 'share_contact') {
191
+ if (!hasOnlyKeys(value, new Set([
192
+ 'action',
193
+ 'conversationId',
194
+ 'contactUserId',
195
+ 'text',
196
+ 'messageId',
197
+ ]))) {
198
+ throw new Error('share_contact contains unsupported fields');
199
+ }
200
+ const conversationId = readRequiredString(value.conversationId);
201
+ const contactUserId = readRequiredString(value.contactUserId);
202
+ const messageId = value.messageId === undefined
203
+ ? undefined
204
+ : readRequiredString(value.messageId);
205
+ if (!conversationId)
206
+ throw new Error('communicate.conversationId is required');
207
+ if (!contactUserId)
208
+ throw new Error('communicate.contactUserId is required');
209
+ if (value.text !== undefined && typeof value.text !== 'string') {
210
+ throw new Error('communicate.text must be a string');
211
+ }
212
+ if (value.messageId !== undefined && !messageId) {
213
+ throw new Error('communicate.messageId must be a non-empty string');
214
+ }
215
+ return {
216
+ action: 'share_contact',
217
+ conversationId,
218
+ contactUserId,
219
+ ...(value.text !== undefined ? { text: value.text } : {}),
220
+ ...(messageId ? { messageId } : {}),
221
+ };
222
+ }
223
+ if (value.action === 'manage_group_members') {
224
+ if (!hasOnlyKeys(value, new Set([
225
+ 'action',
226
+ 'conversationId',
227
+ 'userId',
228
+ 'operation',
229
+ ]))) {
230
+ throw new Error('manage_group_members contains unsupported fields');
231
+ }
232
+ const conversationId = readRequiredString(value.conversationId);
233
+ const userId = readRequiredString(value.userId);
234
+ if (!conversationId)
235
+ throw new Error('communicate.conversationId is required');
236
+ if (!userId)
237
+ throw new Error('communicate.userId is required');
238
+ if (value.operation !== 'add' && value.operation !== 'remove') {
239
+ throw new Error('communicate.operation must be add or remove');
240
+ }
241
+ return {
242
+ action: 'manage_group_members',
243
+ conversationId,
244
+ userId,
245
+ operation: value.operation,
246
+ };
247
+ }
248
+ const text = readRequiredString(value.text);
249
+ const messageId = value.messageId === undefined
250
+ ? undefined
251
+ : readRequiredString(value.messageId);
252
+ if (!text)
253
+ throw new Error('communicate.text is required');
254
+ if (value.messageId !== undefined && !messageId) {
255
+ throw new Error('communicate.messageId must be a non-empty string');
256
+ }
257
+ if (value.action === 'message_existing') {
258
+ if (!hasOnlyKeys(value, new Set(['action', 'conversationId', 'text', 'messageId']))) {
259
+ throw new Error('message_existing contains unsupported fields');
260
+ }
261
+ const conversationId = readRequiredString(value.conversationId);
262
+ if (!conversationId)
263
+ throw new Error('communicate.conversationId is required');
264
+ return {
265
+ action: 'message_existing',
266
+ conversationId,
267
+ text,
268
+ ...(messageId ? { messageId } : {}),
269
+ };
270
+ }
271
+ if (value.action === 'start_direct') {
272
+ if (!hasOnlyKeys(value, new Set(['action', 'principalId', 'text', 'selection', 'messageId']))) {
273
+ throw new Error('start_direct contains unsupported fields');
274
+ }
275
+ const principalId = readRequiredString(value.principalId);
276
+ if (!principalId)
277
+ throw new Error('communicate.principalId is required');
278
+ const selection = value.selection === undefined
279
+ ? undefined
280
+ : parseDirectConversationSelection(value.selection);
281
+ return {
282
+ action: 'start_direct',
283
+ principalId,
284
+ text,
285
+ ...(selection ? { selection } : {}),
286
+ ...(messageId ? { messageId } : {}),
287
+ };
288
+ }
289
+ throw new Error('communicate.action is invalid');
290
+ }
291
+ function renderResult(result) {
292
+ let id;
293
+ if (result.status === 'messaged'
294
+ || result.status === 'forwarded'
295
+ || result.status === 'shared') {
296
+ id = result.messageId;
297
+ }
298
+ else if (result.status === 'added' || result.status === 'removed') {
299
+ id = result.userId;
300
+ }
301
+ else if (result.status === 'created') {
302
+ id = result.conversationId;
303
+ }
304
+ else if (result.status === 'requested' || result.status === 'pending') {
305
+ id = result.requestId;
306
+ }
307
+ return {
308
+ content: [{
309
+ type: 'text',
310
+ text: `communicate: ${result.status}${id ? ` (${id})` : ''}\n${JSON.stringify(result, null, 2)}`,
311
+ }],
312
+ };
313
+ }
314
+ export async function executeCanonCommunicateTool(client, args) {
315
+ let input;
316
+ try {
317
+ input = parseCommunicateToolInput(args);
318
+ }
319
+ catch (error) {
320
+ return {
321
+ content: [{
322
+ type: 'text',
323
+ text: `Invalid communicate arguments: ${error.message}`,
324
+ }],
325
+ isError: true,
326
+ };
327
+ }
328
+ return renderResult(await client.communicate(input));
329
+ }
330
+ /** A one-tool binding that runtimes may mount without adding any other tools. */
331
+ export function createCanonCommunicationBinding(options = {}) {
332
+ const toolName = options.toolName?.trim() || CANON_COMMUNICATE_TOOL_NAME;
333
+ return {
334
+ tools: [canonCommunicateToolDefinition(toolName)],
335
+ isToolName: (name) => name === toolName,
336
+ execute: (client, name, args) => {
337
+ if (name !== toolName) {
338
+ return Promise.resolve({
339
+ content: [{ type: 'text', text: `Unsupported Canon communication tool: ${name}` }],
340
+ isError: true,
341
+ });
342
+ }
343
+ return executeCanonCommunicateTool(client, args);
344
+ },
345
+ };
346
+ }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { CANON_TOOL_VERBS, canonVerbToolDefinitions, executeCanonVerbTool, isCanonToolVerb, normalizeVerbToolArgs, stampSendToTurnComplete, type ExecuteVerbToolOptions, type VerbExecutionContext, type VerbProjectionOptions, type VerbToolDefinition, type VerbToolResult, } from './verb-tools.js';
2
- export { CANON_VERB_MCP_SERVER_NAME, createCanonVerbMcpServer, createOwnerBoundCanonCommunicationMcpServer, type CanonVerbMcpServerOptions, } from './verb-mcp.js';
3
- export { OWNER_BOUND_CANON_COMMUNICATION_VERBS, createOwnerBoundCanonCommunicationBinding, type OwnerBoundCanonCommunicationBinding, type OwnerBoundCanonCommunicationToolNames, type OwnerBoundCanonCommunicationVerb, type OwnerBoundCanonContactTarget, type OwnerBoundCanonTurnContext, } from './owner-bound-communication.js';
1
+ export { CANON_TOOL_VERBS, CANON_COMMUNICATE_REPLACED_VERBS, canonVerbToolDefinitions, executeCanonVerbTool, isCanonToolVerb, isCommunicateReplacedVerb, normalizeVerbToolArgs, type ExecuteVerbToolOptions, type VerbExecutionContext, type VerbProjectionOptions, type VerbToolDefinition, type VerbToolResult, } from './verb-tools.js';
2
+ export { CANON_VERB_MCP_SERVER_NAME, createCanonVerbMcpServer, type CanonVerbMcpServerOptions, } from './verb-mcp.js';
3
+ export { CANON_COMMUNICATE_TOOL_NAME, canonCommunicateToolDefinition, createCanonCommunicationBinding, executeCanonCommunicateTool, parseCommunicateToolInput, type CanonCommunicationBinding, } from './communication-tool.js';
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export { CANON_TOOL_VERBS, canonVerbToolDefinitions, executeCanonVerbTool, isCanonToolVerb, normalizeVerbToolArgs, stampSendToTurnComplete, } from './verb-tools.js';
2
- export { CANON_VERB_MCP_SERVER_NAME, createCanonVerbMcpServer, createOwnerBoundCanonCommunicationMcpServer, } from './verb-mcp.js';
3
- export { OWNER_BOUND_CANON_COMMUNICATION_VERBS, createOwnerBoundCanonCommunicationBinding, } from './owner-bound-communication.js';
1
+ export { CANON_TOOL_VERBS, CANON_COMMUNICATE_REPLACED_VERBS, canonVerbToolDefinitions, executeCanonVerbTool, isCanonToolVerb, isCommunicateReplacedVerb, normalizeVerbToolArgs, } from './verb-tools.js';
2
+ export { CANON_VERB_MCP_SERVER_NAME, createCanonVerbMcpServer, } from './verb-mcp.js';
3
+ export { CANON_COMMUNICATE_TOOL_NAME, canonCommunicateToolDefinition, createCanonCommunicationBinding, executeCanonCommunicateTool, parseCommunicateToolInput, } from './communication-tool.js';
@@ -21,17 +21,14 @@
21
21
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
22
22
  import type { CanonClient, CanonVerbName } from '@canonmsg/core';
23
23
  import { type VerbExecutionContext } from './verb-tools.js';
24
- import { type OwnerBoundCanonTurnContext } from './owner-bound-communication.js';
25
24
  /** MCP server name — verbs appear to the model as `mcp__canon__<verb>`. */
26
25
  export declare const CANON_VERB_MCP_SERVER_NAME = "canon";
27
26
  export interface CanonVerbMcpServerOptions {
28
27
  /**
29
- * Replace the five communication verbs with the strict owner-bound
30
- * projection. Other canonical verbs retain their existing scoped behavior.
28
+ * Add the compact principal-neutral `communicate` tool. The broad `send_to`
29
+ * verb is never model-visible; all conversation-scoped verbs remain unchanged.
31
30
  */
32
- ownerBoundCommunication?: {
33
- getContext: () => OwnerBoundCanonTurnContext | null | undefined;
34
- };
31
+ communication?: boolean;
35
32
  }
36
33
  export declare function createCanonVerbMcpServer(getClient: () => CanonClient | null,
37
34
  /**
@@ -48,9 +45,3 @@ getContext?: () => VerbExecutionContext | undefined,
48
45
  * the model" rather than "silence never happens".
49
46
  */
50
47
  onVerbCall?: (verb: CanonVerbName, args: Record<string, unknown>, context: VerbExecutionContext | undefined) => void, options?: CanonVerbMcpServerOptions): McpServer;
51
- /**
52
- * In-process MCP projection for a foreground owner turn. Unlike the full
53
- * canonical server, this exposes only the admission-aware communication
54
- * surface and rejects calls when the host cannot prove owner/source context.
55
- */
56
- export declare function createOwnerBoundCanonCommunicationMcpServer(getClient: () => CanonClient | null, getContext: () => OwnerBoundCanonTurnContext | null | undefined, onVerbCall?: (verb: CanonVerbName, args: Record<string, unknown>) => void): McpServer;
package/dist/verb-mcp.js CHANGED
@@ -20,8 +20,8 @@
20
20
  */
21
21
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
22
22
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
23
- import { canonVerbToolDefinitions, executeCanonVerbTool, isCanonToolVerb, stampSendToTurnComplete, } from './verb-tools.js';
24
- import { OWNER_BOUND_CANON_COMMUNICATION_VERBS, createOwnerBoundCanonCommunicationBinding, } from './owner-bound-communication.js';
23
+ import { canonVerbToolDefinitions, executeCanonVerbTool, isCanonToolVerb, isCommunicateReplacedVerb, } from './verb-tools.js';
24
+ import { createCanonCommunicationBinding } from './communication-tool.js';
25
25
  /** MCP server name — verbs appear to the model as `mcp__canon__<verb>`. */
26
26
  export const CANON_VERB_MCP_SERVER_NAME = 'canon';
27
27
  export function createCanonVerbMcpServer(getClient,
@@ -46,27 +46,21 @@ onVerbCall, options = {}) {
46
46
  // tool handlers).
47
47
  const mcpServer = new McpServer({ name: CANON_VERB_MCP_SERVER_NAME, version: '1.0.0' }, { capabilities: { tools: {} } });
48
48
  const server = mcpServer.server;
49
+ const communication = createCanonCommunicationBinding();
50
+ const compactCommunication = options.communication !== undefined;
49
51
  // The projection must tell the model what the dispatch below actually
50
52
  // does: waiting posture (waitForResult: true), and conversation-scoped
51
53
  // exactly when the binding supplies a context.
52
- const ownerBinding = options.ownerBoundCommunication
53
- ? createOwnerBoundCanonCommunicationBinding()
54
- : null;
55
- server.setRequestHandler(ListToolsRequestSchema, async () => {
56
- const canonical = canonVerbToolDefinitions({
57
- interaction: 'waiting',
58
- conversationScoped: Boolean(getContext),
59
- });
60
- if (!ownerBinding)
61
- return { tools: canonical };
62
- const ownerVerbs = new Set(OWNER_BOUND_CANON_COMMUNICATION_VERBS);
63
- return {
64
- tools: [
65
- ...canonical.filter((definition) => !ownerVerbs.has(definition.name)),
66
- ...ownerBinding.tools,
67
- ],
68
- };
69
- });
54
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
55
+ tools: [
56
+ ...canonVerbToolDefinitions({
57
+ interaction: 'waiting',
58
+ conversationScoped: Boolean(getContext),
59
+ compactCommunication,
60
+ }),
61
+ ...(options.communication ? communication.tools : []),
62
+ ],
63
+ }));
70
64
  server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
71
65
  const client = getClient();
72
66
  if (!client) {
@@ -76,7 +70,15 @@ onVerbCall, options = {}) {
76
70
  };
77
71
  }
78
72
  const name = request.params.name;
79
- if (!isCanonToolVerb(name)) {
73
+ if (options.communication && communication.isToolName(name)) {
74
+ const result = await communication.execute(client, name, request.params.arguments);
75
+ return {
76
+ content: result.content.map((item) => ({ type: 'text', text: item.text })),
77
+ ...(result.isError ? { isError: true } : {}),
78
+ };
79
+ }
80
+ if (!isCanonToolVerb(name)
81
+ || (compactCommunication && isCommunicateReplacedVerb(name))) {
80
82
  return {
81
83
  content: [{ type: 'text', text: `Unknown tool: ${name}` }],
82
84
  isError: true,
@@ -85,25 +87,7 @@ onVerbCall, options = {}) {
85
87
  const rawArgs = request.params.arguments && typeof request.params.arguments === 'object'
86
88
  ? { ...request.params.arguments }
87
89
  : {};
88
- if (ownerBinding && ownerBinding.verbForToolName(name)) {
89
- try {
90
- onVerbCall?.(name, rawArgs, getContext?.());
91
- }
92
- catch {
93
- // A host bookkeeping bug must never break tool dispatch.
94
- }
95
- const result = await ownerBinding.execute({
96
- client,
97
- toolName: name,
98
- arguments: rawArgs,
99
- context: options.ownerBoundCommunication.getContext(),
100
- });
101
- return {
102
- content: result.content.map((item) => ({ type: 'text', text: item.text })),
103
- ...(result.isError ? { isError: true } : {}),
104
- };
105
- }
106
- const verbArgs = name === 'send_to' ? stampSendToTurnComplete(rawArgs) : rawArgs;
90
+ const verbArgs = rawArgs;
107
91
  const context = getContext?.();
108
92
  try {
109
93
  onVerbCall?.(name, verbArgs, context);
@@ -123,50 +107,3 @@ onVerbCall, options = {}) {
123
107
  });
124
108
  return mcpServer;
125
109
  }
126
- /**
127
- * In-process MCP projection for a foreground owner turn. Unlike the full
128
- * canonical server, this exposes only the admission-aware communication
129
- * surface and rejects calls when the host cannot prove owner/source context.
130
- */
131
- export function createOwnerBoundCanonCommunicationMcpServer(getClient, getContext, onVerbCall) {
132
- const binding = createOwnerBoundCanonCommunicationBinding();
133
- const mcpServer = new McpServer({ name: CANON_VERB_MCP_SERVER_NAME, version: '1.0.0' }, { capabilities: { tools: {} } });
134
- const server = mcpServer.server;
135
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: binding.tools }));
136
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
137
- const client = getClient();
138
- if (!client) {
139
- return {
140
- content: [{ type: 'text', text: 'Canon not connected' }],
141
- isError: true,
142
- };
143
- }
144
- const verb = binding.verbForToolName(request.params.name);
145
- if (!verb) {
146
- return {
147
- content: [{ type: 'text', text: `Unknown tool: ${request.params.name}` }],
148
- isError: true,
149
- };
150
- }
151
- const args = request.params.arguments && typeof request.params.arguments === 'object'
152
- ? { ...request.params.arguments }
153
- : {};
154
- try {
155
- onVerbCall?.(verb, args);
156
- }
157
- catch {
158
- // Host bookkeeping cannot break dispatch.
159
- }
160
- const result = await binding.execute({
161
- client,
162
- toolName: request.params.name,
163
- arguments: args,
164
- context: getContext(),
165
- });
166
- return {
167
- content: result.content.map((item) => ({ type: 'text', text: item.text })),
168
- ...(result.isError ? { isError: true } : {}),
169
- };
170
- });
171
- return mcpServer;
172
- }
@@ -25,6 +25,9 @@
25
25
  */
26
26
  import { type CanonClient, type CanonVerbName } from '@canonmsg/core';
27
27
  export declare const CANON_TOOL_VERBS: CanonVerbName[];
28
+ /** Canonical operations represented by the single model-facing communicate tool. */
29
+ export declare const CANON_COMMUNICATE_REPLACED_VERBS: readonly ["share_contact", "add_member", "remove_member", "leave_conversation", "list_contacts", "list_conversations"];
30
+ export declare function isCommunicateReplacedVerb(name: string): name is CanonVerbName;
28
31
  export interface VerbToolDefinition {
29
32
  name: string;
30
33
  description: string;
@@ -43,17 +46,15 @@ export interface VerbProjectionOptions {
43
46
  * (default) mark conversationId required from the model instead.
44
47
  */
45
48
  conversationScoped?: boolean;
49
+ /**
50
+ * Keep social operations behind the single `communicate` tool. Canonical
51
+ * verbs remain executable internally but are omitted from the model schema.
52
+ */
53
+ compactCommunication?: boolean;
46
54
  }
47
55
  /** MCP tool definitions projected from the contract, shaped per binding. */
48
56
  export declare function canonVerbToolDefinitions(options?: VerbProjectionOptions): VerbToolDefinition[];
49
57
  export declare function isCanonToolVerb(name: string): name is CanonVerbName;
50
- /**
51
- * Outbound sends from a Claude surface are completed turns: stamp
52
- * turnSemantics 'turn_complete' into send_to messageOptions.metadata unless
53
- * the caller already set turn semantics. Shared by the channel server and
54
- * the host-mode MCP mount.
55
- */
56
- export declare function stampSendToTurnComplete(args: Record<string, unknown>): Record<string, unknown>;
57
58
  export interface VerbToolResult {
58
59
  content: Array<{
59
60
  type: 'text';
@@ -79,18 +80,6 @@ export interface VerbExecutionContext {
79
80
  responseUserId?: string;
80
81
  /** Canon message id of the inbound message that triggered this turn. */
81
82
  sourceMessageId?: string;
82
- /**
83
- * Trusted provenance for a host-bound send_to projection. When present,
84
- * these fields replace any same-named model-authored values. Bindings that
85
- * expose the full canonical schema simply omit it and retain existing
86
- * unscoped send_to behavior.
87
- */
88
- sendTo?: {
89
- sourceConversationId: string;
90
- sourceMessageId?: string;
91
- turnId?: string;
92
- turnSemantics: 'turn_complete';
93
- };
94
83
  }
95
84
  export interface ExecuteVerbToolOptions {
96
85
  /**
@@ -28,10 +28,6 @@ import { findVerbByteLimitViolations, getVerbInputSchema, projectVerbIntentToWir
28
28
  import { RUNTIME_CARD_JSON_SCHEMA_V1 } from '@canonmsg/rich-cards';
29
29
  /** Verbs exposed to models, with binding-level descriptions. */
30
30
  const CANON_VERB_TOOL_DESCRIPTIONS = {
31
- send_to: 'Send a Canon message to another conversation or user. Supports a private '
32
- + 'selfContext note (with sourceConversationId) so you recognize your own '
33
- + 'cross-conversation transfer later. Admission-aware for user targets: '
34
- + 'may return requested/pending (contact request) instead of messaged.',
35
31
  request_input: 'Ask a human in a Canon conversation a structured question (input card).',
36
32
  request_approval: 'Ask a human to allow/deny an action via a Canon approval card. Use '
37
33
  + "mode 'detached' and poll with check_approval for long-lived approvals.",
@@ -43,17 +39,11 @@ const CANON_VERB_TOOL_DESCRIPTIONS = {
43
39
  share_contact: "Share a contact card into a conversation. The shared user must be in "
44
40
  + 'your contacts.',
45
41
  react: 'Toggle an emoji reaction on a Canon message.',
46
- forward: 'Forward an existing Canon message into another conversation, with an optional caption.',
47
- create_group: 'Create a Canon group conversation. Directly-addable members join at '
48
- + 'creation; policy approval and agent-owner setup become pending invites '
49
- + '(result.pending); hard-denied members are skipped (result.skipped).',
50
42
  add_member: 'Add a member to a Canon group (policy approval and/or agent-owner setup may return a pending invite).',
51
43
  remove_member: 'Remove a member from a Canon group (requires owner/admin role).',
52
44
  leave_conversation: 'Leave a Canon group conversation.',
53
45
  list_contacts: 'List your Canon contacts.',
54
- list_contact_requests: 'List contact requests. Defaults to pending inbound; use outbound with includeResolved for reconnect recovery.',
55
46
  list_conversations: 'List your Canon conversations (optionally limited).',
56
- cancel_contact_request: 'Cancel your own pending direct-message contact request.',
57
47
  no_reply: 'End your turn without posting anything to the conversation. Use it in '
58
48
  + 'groups when you have nothing to add — no message is created, so no '
59
49
  + 'other member or agent is triggered. Optional private reason (logged, '
@@ -80,12 +70,28 @@ const INTERACTION_POSTURE_NOTES = {
80
70
  },
81
71
  };
82
72
  export const CANON_TOOL_VERBS = Object.keys(CANON_VERB_TOOL_DESCRIPTIONS);
73
+ /** Canonical operations represented by the single model-facing communicate tool. */
74
+ export const CANON_COMMUNICATE_REPLACED_VERBS = [
75
+ 'share_contact',
76
+ 'add_member',
77
+ 'remove_member',
78
+ 'leave_conversation',
79
+ 'list_contacts',
80
+ 'list_conversations',
81
+ ];
82
+ const COMMUNICATE_REPLACED_VERB_SET = new Set(CANON_COMMUNICATE_REPLACED_VERBS);
83
+ export function isCommunicateReplacedVerb(name) {
84
+ return COMMUNICATE_REPLACED_VERB_SET.has(name);
85
+ }
83
86
  const CARD_VERBS = new Set(['send_card', 'request_card']);
84
87
  const RUNTIME_OWNED_TOOL_FIELDS = ['native', 'runtimeId', 'turnId'];
85
88
  /** MCP tool definitions projected from the contract, shaped per binding. */
86
89
  export function canonVerbToolDefinitions(options = {}) {
87
90
  const interaction = options.interaction ?? 'notify';
88
- return CANON_TOOL_VERBS.map((verb) => {
91
+ const projectedVerbs = options.compactCommunication
92
+ ? CANON_TOOL_VERBS.filter((verb) => !COMMUNICATE_REPLACED_VERB_SET.has(verb))
93
+ : CANON_TOOL_VERBS;
94
+ return projectedVerbs.map((verb) => {
89
95
  let inputSchema = getVerbInputSchema(verb, CARD_VERBS.has(verb) ? { cardSchema: RUNTIME_CARD_JSON_SCHEMA_V1 } : undefined);
90
96
  if (isInteractionVerb(verb)) {
91
97
  const properties = {
@@ -125,20 +131,6 @@ export function canonVerbToolDefinitions(options = {}) {
125
131
  export function isCanonToolVerb(name) {
126
132
  return CANON_TOOL_VERBS.includes(name);
127
133
  }
128
- /**
129
- * Outbound sends from a Claude surface are completed turns: stamp
130
- * turnSemantics 'turn_complete' into send_to messageOptions.metadata unless
131
- * the caller already set turn semantics. Shared by the channel server and
132
- * the host-mode MCP mount.
133
- */
134
- export function stampSendToTurnComplete(args) {
135
- const options = { ...(args.messageOptions ?? {}) };
136
- options.metadata = {
137
- turnSemantics: 'turn_complete',
138
- ...(options.metadata ?? {}),
139
- };
140
- return { ...args, messageOptions: options };
141
- }
142
134
  /** Verbs whose intents carry context-bound fields the binding must complete. */
143
135
  const INTERACTION_VERBS = new Set([
144
136
  'request_input',
@@ -181,34 +173,6 @@ const DEFAULT_INTERACTION_TIMEOUT_MS = 5 * 60 * 1000;
181
173
  * without a conversation.
182
174
  */
183
175
  export function normalizeVerbToolArgs(verb, args, context, now = Date.now()) {
184
- if (verb === 'send_to' && context?.sendTo) {
185
- const messageOptions = isRecord(args.messageOptions) ? { ...args.messageOptions } : {};
186
- const metadata = isRecord(messageOptions.metadata) ? { ...messageOptions.metadata } : {};
187
- // These names are host-owned on a bound projection. Delete first so an
188
- // absent optional trusted value cannot leave a forged model value behind.
189
- delete metadata.sourceConversationId;
190
- delete metadata.sourceMessageId;
191
- delete metadata.turnId;
192
- delete metadata.turnSemantics;
193
- return {
194
- args: {
195
- ...args,
196
- sourceConversationId: context.sendTo.sourceConversationId,
197
- messageOptions: {
198
- ...messageOptions,
199
- metadata: {
200
- ...metadata,
201
- sourceConversationId: context.sendTo.sourceConversationId,
202
- ...(context.sendTo.sourceMessageId
203
- ? { sourceMessageId: context.sendTo.sourceMessageId }
204
- : {}),
205
- ...(context.sendTo.turnId ? { turnId: context.sendTo.turnId } : {}),
206
- turnSemantics: context.sendTo.turnSemantics,
207
- },
208
- },
209
- },
210
- };
211
- }
212
176
  if (verb === 'no_reply') {
213
177
  const completed = { ...args };
214
178
  // messageId is binding-owned (like turnId on interaction verbs): the
@@ -289,9 +253,6 @@ export function normalizeVerbToolArgs(verb, args, context, now = Date.now()) {
289
253
  }
290
254
  return { args: completed };
291
255
  }
292
- function isRecord(value) {
293
- return Boolean(value && typeof value === 'object' && !Array.isArray(value));
294
- }
295
256
  const DEFAULT_POLL_MS = 1000;
296
257
  const DEFAULT_MAX_WAIT_MS = 30 * 60 * 1000;
297
258
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -408,23 +369,6 @@ export async function executeCanonVerbTool(client, verb, args, options = {}) {
408
369
  return { content: [{ type: 'text', text: `Invalid arguments: ${normalized.error}` }], isError: true };
409
370
  }
410
371
  args = normalized.args;
411
- // canonContactId is a contact-card identity, not a wire field: resolve it
412
- // to a targetUserId here (the two-step the contract documents) so the
413
- // advertised send_to schema is honest in every binding.
414
- if (verb === 'send_to' && typeof args.canonContactId === 'string') {
415
- const resolved = await client.resolveAdmission({ canonContactId: args.canonContactId });
416
- if (!resolved.resolvedTargetUserId) {
417
- return {
418
- content: [{
419
- type: 'text',
420
- text: `send_to: unavailable\n${JSON.stringify({ status: 'unavailable', reason: 'canonContactId could not be resolved' }, null, 2)}`,
421
- }],
422
- isError: true,
423
- };
424
- }
425
- const { canonContactId: _resolved, ...rest } = args;
426
- args = { ...rest, targetUserId: resolved.resolvedTargetUserId };
427
- }
428
372
  const violations = findVerbByteLimitViolations(verb, args);
429
373
  if (violations.length > 0) {
430
374
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-tools",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Canonical Canon verb tools — shared projections of canon.verbs.v1 for runtime bindings",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -17,6 +17,7 @@
17
17
  "scripts": {
18
18
  "prepare:workspace-deps": "node ../../scripts/run-workspace-prep.mjs ../core ../rich-cards",
19
19
  "build": "npm run prepare:workspace-deps && node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
20
+ "sync:hermes-schema": "npm run build && node scripts/sync-hermes-communication-schema.mjs",
20
21
  "dev": "tsc --watch",
21
22
  "test": "vitest run",
22
23
  "prepack": "npm run build"
@@ -40,8 +41,8 @@
40
41
  "access": "public"
41
42
  },
42
43
  "dependencies": {
43
- "@canonmsg/core": "^10.7.0",
44
- "@canonmsg/rich-cards": "^0.10.2",
44
+ "@canonmsg/core": "^12.0.0",
45
+ "@canonmsg/rich-cards": "^0.10.4",
45
46
  "@modelcontextprotocol/sdk": "^1.30.0"
46
47
  },
47
48
  "devDependencies": {
@@ -1,36 +0,0 @@
1
- import type { CanonClient } from '@canonmsg/core';
2
- import { type VerbToolDefinition, type VerbToolResult } from './verb-tools.js';
3
- export declare const OWNER_BOUND_CANON_COMMUNICATION_VERBS: readonly ["send_to", "list_contacts", "list_contact_requests", "list_conversations", "cancel_contact_request"];
4
- export type OwnerBoundCanonCommunicationVerb = (typeof OWNER_BOUND_CANON_COMMUNICATION_VERBS)[number];
5
- export interface OwnerBoundCanonContactTarget {
6
- targetUserId: string;
7
- canonContactId?: string;
8
- sourceCardMessageId?: string;
9
- }
10
- /** Trusted per-turn state supplied by a Canon host, never by the model. */
11
- export interface OwnerBoundCanonTurnContext {
12
- isOwnerTurn: boolean;
13
- conversationId: string;
14
- sourceMessageId: string;
15
- turnId?: string;
16
- replyContactTarget?: OwnerBoundCanonContactTarget;
17
- }
18
- export type OwnerBoundCanonCommunicationToolNames = Record<OwnerBoundCanonCommunicationVerb, string>;
19
- export interface OwnerBoundCanonCommunicationBinding {
20
- tools: ReadonlyArray<VerbToolDefinition>;
21
- verbForToolName(toolName: string): OwnerBoundCanonCommunicationVerb | null;
22
- execute(input: {
23
- client: CanonClient;
24
- toolName: string;
25
- arguments: unknown;
26
- context: OwnerBoundCanonTurnContext | null | undefined;
27
- }): Promise<VerbToolResult>;
28
- }
29
- /**
30
- * Build the smallest owner-authorized Canon communication surface shared by
31
- * model hosts. The projection, name mapping, validation, target binding, and
32
- * trusted source injection live here so integrations cannot drift.
33
- */
34
- export declare function createOwnerBoundCanonCommunicationBinding(options?: {
35
- toolNames?: Partial<OwnerBoundCanonCommunicationToolNames>;
36
- }): OwnerBoundCanonCommunicationBinding;
@@ -1,163 +0,0 @@
1
- import { canonVerbToolDefinitions, executeCanonVerbTool, } from './verb-tools.js';
2
- export const OWNER_BOUND_CANON_COMMUNICATION_VERBS = [
3
- 'send_to',
4
- 'list_contacts',
5
- 'list_contact_requests',
6
- 'list_conversations',
7
- 'cancel_contact_request',
8
- ];
9
- const DEFAULT_TOOL_NAMES = {
10
- send_to: 'send_to',
11
- list_contacts: 'list_contacts',
12
- list_contact_requests: 'list_contact_requests',
13
- list_conversations: 'list_conversations',
14
- cancel_contact_request: 'cancel_contact_request',
15
- };
16
- const CANONICAL_DEFINITIONS = new Map(canonVerbToolDefinitions().map((definition) => [definition.name, definition]));
17
- function isRecord(value) {
18
- return Boolean(value && typeof value === 'object' && !Array.isArray(value));
19
- }
20
- function readNonEmptyString(value) {
21
- return typeof value === 'string' && value.trim() ? value.trim() : null;
22
- }
23
- function canonicalProperty(verb, property) {
24
- const properties = CANONICAL_DEFINITIONS.get(verb)?.inputSchema.properties;
25
- if (!isRecord(properties))
26
- return {};
27
- return isRecord(properties[property]) ? properties[property] : {};
28
- }
29
- function narrowSendToDefinition(name) {
30
- const canonical = CANONICAL_DEFINITIONS.get('send_to');
31
- return {
32
- name,
33
- description: `${canonical?.description ?? 'Send a Canon message.'} `
34
- + 'This owner-bound projection accepts only one contact target and visible message text. '
35
- + 'Canon supplies trusted source routing and may replace the target with the contact card '
36
- + 'replied to by the owner. requested and pending are final results for this call; do not retry.',
37
- inputSchema: {
38
- type: 'object',
39
- additionalProperties: false,
40
- properties: {
41
- targetUserId: {
42
- ...canonicalProperty('send_to', 'targetUserId'),
43
- description: 'Target Canon user id. Use exactly one target field.',
44
- },
45
- canonContactId: {
46
- ...canonicalProperty('send_to', 'canonContactId'),
47
- description: 'Stable id from a Canon contact card. Use exactly one target field.',
48
- },
49
- text: {
50
- ...canonicalProperty('send_to', 'text'),
51
- minLength: 1,
52
- description: 'Visible first message to the target.',
53
- },
54
- requestMessage: {
55
- ...canonicalProperty('send_to', 'requestMessage'),
56
- description: 'Optional note shown to the target human owner when approval is required.',
57
- },
58
- },
59
- required: ['text'],
60
- oneOf: [
61
- { required: ['targetUserId'], not: { required: ['canonContactId'] } },
62
- { required: ['canonContactId'], not: { required: ['targetUserId'] } },
63
- ],
64
- },
65
- };
66
- }
67
- function parseNarrowSendToArguments(value, boundTarget) {
68
- if (!isRecord(value))
69
- return null;
70
- const allowed = new Set(['targetUserId', 'canonContactId', 'text', 'requestMessage']);
71
- if (Object.keys(value).some((key) => !allowed.has(key)))
72
- return null;
73
- const targetUserId = readNonEmptyString(value.targetUserId);
74
- const canonContactId = readNonEmptyString(value.canonContactId);
75
- const text = readNonEmptyString(value.text);
76
- const requestMessage = readNonEmptyString(value.requestMessage);
77
- if (!text || (!boundTarget && ((!targetUserId && !canonContactId) || (targetUserId && canonContactId)))) {
78
- return null;
79
- }
80
- return {
81
- ...(boundTarget?.canonContactId
82
- ? { canonContactId: boundTarget.canonContactId }
83
- : boundTarget
84
- ? { targetUserId: boundTarget.targetUserId }
85
- : targetUserId
86
- ? { targetUserId }
87
- : { canonContactId }),
88
- text,
89
- ...(requestMessage ? { requestMessage } : {}),
90
- };
91
- }
92
- function invalidResult(message) {
93
- return {
94
- content: [{ type: 'text', text: message }],
95
- isError: true,
96
- };
97
- }
98
- /**
99
- * Build the smallest owner-authorized Canon communication surface shared by
100
- * model hosts. The projection, name mapping, validation, target binding, and
101
- * trusted source injection live here so integrations cannot drift.
102
- */
103
- export function createOwnerBoundCanonCommunicationBinding(options = {}) {
104
- const toolNames = { ...DEFAULT_TOOL_NAMES, ...options.toolNames };
105
- const verbByToolName = new Map();
106
- for (const verb of OWNER_BOUND_CANON_COMMUNICATION_VERBS) {
107
- const name = toolNames[verb];
108
- if (!name || verbByToolName.has(name)) {
109
- throw new Error(`Owner-bound Canon communication tool name is missing or duplicated: ${name}`);
110
- }
111
- verbByToolName.set(name, verb);
112
- }
113
- const tools = OWNER_BOUND_CANON_COMMUNICATION_VERBS.map((verb) => {
114
- if (verb === 'send_to')
115
- return narrowSendToDefinition(toolNames[verb]);
116
- const canonical = CANONICAL_DEFINITIONS.get(verb);
117
- if (!canonical)
118
- throw new Error(`Missing canonical Canon verb definition: ${verb}`);
119
- return {
120
- ...canonical,
121
- name: toolNames[verb],
122
- description: verb === 'cancel_contact_request'
123
- ? `${canonical.description} Only a pending outbound DM request can be cancelled.`
124
- : canonical.description,
125
- };
126
- });
127
- return {
128
- tools,
129
- verbForToolName: (toolName) => verbByToolName.get(toolName) ?? null,
130
- execute: async ({ client, toolName, arguments: rawArguments, context }) => {
131
- const verb = verbByToolName.get(toolName);
132
- if (!verb)
133
- return invalidResult(`Unsupported Canon communication tool: ${toolName}`);
134
- if (!context?.isOwnerTurn) {
135
- return invalidResult('Canon communication tools require an owner-authored foreground turn.');
136
- }
137
- if (!context.conversationId || !context.sourceMessageId) {
138
- return invalidResult('Canon communication tools require trusted source conversation and message context.');
139
- }
140
- if (!isRecord(rawArguments))
141
- return invalidResult(`Invalid ${verb} arguments.`);
142
- let args = { ...rawArguments };
143
- let executionContext;
144
- if (verb === 'send_to') {
145
- const parsed = parseNarrowSendToArguments(rawArguments, context.replyContactTarget);
146
- if (!parsed)
147
- return invalidResult('Invalid send_to arguments.');
148
- args = parsed;
149
- executionContext = {
150
- sendTo: {
151
- sourceConversationId: context.conversationId,
152
- sourceMessageId: context.sourceMessageId,
153
- ...(context.turnId ? { turnId: context.turnId } : {}),
154
- turnSemantics: 'turn_complete',
155
- },
156
- };
157
- }
158
- return executeCanonVerbTool(client, verb, args, {
159
- ...(executionContext ? { context: executionContext } : {}),
160
- });
161
- },
162
- };
163
- }