@canonmsg/agent-tools 0.7.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
@@ -4,10 +4,11 @@ Model-facing tool definitions for the canonical Canon verbs, plus the dispatch t
4
4
 
5
5
  This is the model-facing projection of Canon's verb layer. Everyday outbound
6
6
  communication uses one `communicate` tool for existing messages, new direct
7
- conversations, group creation, and forwarding. Interaction, card, reaction,
8
- contact-sharing, group-management, read, and `no_reply` verbs remain separate
9
- where their model interaction is materially different. Everything dispatches
10
- through the canonical `POST /agent/verbs/:verb` endpoints.
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.
11
12
 
12
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.
13
14
 
@@ -36,7 +36,8 @@ export function canonCommunicateToolDefinition(name = CANON_COMMUNICATE_TOOL_NAM
36
36
  return {
37
37
  name,
38
38
  description: 'Communicate in Canon. Message a known conversation, start/continue a '
39
- + 'direct conversation, create a group, or forward an exact message. Canon '
39
+ + 'direct conversation, create a group, forward an exact message, share a '
40
+ + 'contact, or manage group members. Canon '
40
41
  + 'enforces the recipient and agent-deployer policies; a new direct '
41
42
  + 'conversation or group member may require approval.',
42
43
  inputSchema: {
@@ -98,6 +99,29 @@ export function canonCommunicateToolDefinition(name = CANON_COMMUNICATE_TOOL_NAM
98
99
  text: OPTIONAL_CAPTION,
99
100
  },
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
+ },
101
125
  ],
102
126
  },
103
127
  };
@@ -163,6 +187,64 @@ export function parseCommunicateToolInput(value) {
163
187
  ...(value.text !== undefined ? { text: value.text } : {}),
164
188
  };
165
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
+ }
166
248
  const text = readRequiredString(value.text);
167
249
  const messageId = value.messageId === undefined
168
250
  ? undefined
@@ -207,16 +289,21 @@ export function parseCommunicateToolInput(value) {
207
289
  throw new Error('communicate.action is invalid');
208
290
  }
209
291
  function renderResult(result) {
210
- const id = result.status === 'messaged'
211
- ? result.messageId
212
- : result.status === 'forwarded'
213
- ? result.messageId
214
- : result.status === 'created'
215
- ? result.conversationId
216
- : result.status === 'requested'
217
- || result.status === 'pending'
218
- ? result.requestId
219
- : undefined;
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
+ }
220
307
  return {
221
308
  content: [{
222
309
  type: 'text',
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { CANON_TOOL_VERBS, canonVerbToolDefinitions, executeCanonVerbTool, isCanonToolVerb, normalizeVerbToolArgs, type ExecuteVerbToolOptions, type VerbExecutionContext, type VerbProjectionOptions, type VerbToolDefinition, type VerbToolResult, } from './verb-tools.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
2
  export { CANON_VERB_MCP_SERVER_NAME, createCanonVerbMcpServer, type CanonVerbMcpServerOptions, } from './verb-mcp.js';
3
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, } from './verb-tools.js';
1
+ export { CANON_TOOL_VERBS, CANON_COMMUNICATE_REPLACED_VERBS, canonVerbToolDefinitions, executeCanonVerbTool, isCanonToolVerb, isCommunicateReplacedVerb, normalizeVerbToolArgs, } from './verb-tools.js';
2
2
  export { CANON_VERB_MCP_SERVER_NAME, createCanonVerbMcpServer, } from './verb-mcp.js';
3
3
  export { CANON_COMMUNICATE_TOOL_NAME, canonCommunicateToolDefinition, createCanonCommunicationBinding, executeCanonCommunicateTool, parseCommunicateToolInput, } from './communication-tool.js';
package/dist/verb-mcp.js CHANGED
@@ -20,7 +20,7 @@
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, } from './verb-tools.js';
23
+ import { canonVerbToolDefinitions, executeCanonVerbTool, isCanonToolVerb, isCommunicateReplacedVerb, } from './verb-tools.js';
24
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';
@@ -47,6 +47,7 @@ onVerbCall, options = {}) {
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
49
  const communication = createCanonCommunicationBinding();
50
+ const compactCommunication = options.communication !== undefined;
50
51
  // The projection must tell the model what the dispatch below actually
51
52
  // does: waiting posture (waitForResult: true), and conversation-scoped
52
53
  // exactly when the binding supplies a context.
@@ -55,6 +56,7 @@ onVerbCall, options = {}) {
55
56
  ...canonVerbToolDefinitions({
56
57
  interaction: 'waiting',
57
58
  conversationScoped: Boolean(getContext),
59
+ compactCommunication,
58
60
  }),
59
61
  ...(options.communication ? communication.tools : []),
60
62
  ],
@@ -75,7 +77,8 @@ onVerbCall, options = {}) {
75
77
  ...(result.isError ? { isError: true } : {}),
76
78
  };
77
79
  }
78
- if (!isCanonToolVerb(name)) {
80
+ if (!isCanonToolVerb(name)
81
+ || (compactCommunication && isCommunicateReplacedVerb(name))) {
79
82
  return {
80
83
  content: [{ type: 'text', text: `Unknown tool: ${name}` }],
81
84
  isError: true,
@@ -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,6 +46,11 @@ 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[];
@@ -43,7 +43,6 @@ const CANON_VERB_TOOL_DESCRIPTIONS = {
43
43
  remove_member: 'Remove a member from a Canon group (requires owner/admin role).',
44
44
  leave_conversation: 'Leave a Canon group conversation.',
45
45
  list_contacts: 'List your Canon contacts.',
46
- list_contact_requests: 'List pending inbound contact requests (read-only awareness).',
47
46
  list_conversations: 'List your Canon conversations (optionally limited).',
48
47
  no_reply: 'End your turn without posting anything to the conversation. Use it in '
49
48
  + 'groups when you have nothing to add — no message is created, so no '
@@ -71,12 +70,28 @@ const INTERACTION_POSTURE_NOTES = {
71
70
  },
72
71
  };
73
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
+ }
74
86
  const CARD_VERBS = new Set(['send_card', 'request_card']);
75
87
  const RUNTIME_OWNED_TOOL_FIELDS = ['native', 'runtimeId', 'turnId'];
76
88
  /** MCP tool definitions projected from the contract, shaped per binding. */
77
89
  export function canonVerbToolDefinitions(options = {}) {
78
90
  const interaction = options.interaction ?? 'notify';
79
- 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) => {
80
95
  let inputSchema = getVerbInputSchema(verb, CARD_VERBS.has(verb) ? { cardSchema: RUNTIME_CARD_JSON_SCHEMA_V1 } : undefined);
81
96
  if (isInteractionVerb(verb)) {
82
97
  const properties = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-tools",
3
- "version": "0.7.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": "^11.0.0",
44
- "@canonmsg/rich-cards": "^0.10.3",
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": {