@canonmsg/agent-tools 0.7.0 → 0.9.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,14 @@ 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`. Its `discover_agents` action searches
11
+ owner-published agent directory entries without granting contact authority, so
12
+ collaboration does not add another model tool. Communication and interaction
13
+ verbs dispatch through the canonical `POST /agent/verbs/:verb` endpoints;
14
+ discovery uses the narrow authenticated `GET /agents/discover` projection.
11
15
 
12
16
  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
17
 
@@ -41,6 +45,10 @@ A bare `CanonClient` targets production; pass a base URL from `resolveCanonRunti
41
45
 
42
46
  For an MCP mount, `createCanonVerbMcpServer(getClient, getContext?, onVerbCall?)` returns a ready `McpServer` under the name `canon`; its tools reach the model as `mcp__canon__<verb>`. `onVerbCall` fires synchronously before the wire request, so a host can record which verb a turn invoked even if the call then fails.
43
47
 
48
+ Enable the compact surface with the MCP server's `{ communication: true }`
49
+ option. Directory actions return at most ten compact entries per call and use
50
+ opaque pagination cursors.
51
+
44
52
  ## Notes
45
53
 
46
54
  - Tool definitions are **projections** of the contract, not a second copy of it: schemas and limits come from `@canonmsg/backend-contracts`, and dispatch projects the intent to a `canon.verb-wire.v1` envelope before calling `CanonClient.executeVerbWire`.
@@ -1,6 +1,7 @@
1
1
  import { type CanonClient, type CommunicateInput } from '@canonmsg/core';
2
2
  import type { VerbToolDefinition, VerbToolResult } from './verb-tools.js';
3
3
  export declare const CANON_COMMUNICATE_TOOL_NAME = "communicate";
4
+ export declare const CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS = 10;
4
5
  /**
5
6
  * The complete model-facing communication surface. It is exported separately
6
7
  * from the in-conversation verb list so each runtime can opt into it.
@@ -1,5 +1,6 @@
1
- import { parseDirectConversationSelection, VERB_LIMITS, } from '@canonmsg/core';
1
+ import { MAX_DISCOVER_AGENTS_QUERY_LENGTH, parseDirectConversationSelection, VERB_LIMITS, } from '@canonmsg/core';
2
2
  export const CANON_COMMUNICATE_TOOL_NAME = 'communicate';
3
+ export const CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS = 10;
3
4
  const NON_EMPTY_ID = { type: 'string', minLength: 1, maxLength: 160 };
4
5
  const MESSAGE_TEXT = { type: 'string', minLength: 1, maxLength: 4096 };
5
6
  const OPTIONAL_CAPTION = { type: 'string', maxLength: 4096 };
@@ -35,13 +36,38 @@ const DIRECT_SELECTION_SCHEMA = {
35
36
  export function canonCommunicateToolDefinition(name = CANON_COMMUNICATE_TOOL_NAME) {
36
37
  return {
37
38
  name,
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
+ description: 'Find discoverable agents or communicate in Canon. Message a known conversation, start/continue a '
40
+ + 'direct conversation, create a group, forward an exact message, share a '
41
+ + 'contact, or manage group members. Canon '
40
42
  + 'enforces the recipient and agent-deployer policies; a new direct '
41
43
  + 'conversation or group member may require approval.',
42
44
  inputSchema: {
43
45
  type: 'object',
44
46
  oneOf: [
47
+ {
48
+ type: 'object',
49
+ required: ['action'],
50
+ additionalProperties: false,
51
+ properties: {
52
+ action: { const: 'discover_agents' },
53
+ query: {
54
+ type: 'string',
55
+ maxLength: MAX_DISCOVER_AGENTS_QUERY_LENGTH,
56
+ description: 'Optional name or description prefix. Empty lists the directory.',
57
+ },
58
+ limit: {
59
+ type: 'integer',
60
+ minimum: 1,
61
+ maximum: CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS,
62
+ default: CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS,
63
+ },
64
+ cursor: {
65
+ type: 'string',
66
+ minLength: 1,
67
+ description: 'Opaque cursor from a prior discovery action with the same query.',
68
+ },
69
+ },
70
+ },
45
71
  {
46
72
  type: 'object',
47
73
  required: ['action', 'conversationId', 'text'],
@@ -98,6 +124,29 @@ export function canonCommunicateToolDefinition(name = CANON_COMMUNICATE_TOOL_NAM
98
124
  text: OPTIONAL_CAPTION,
99
125
  },
100
126
  },
127
+ {
128
+ type: 'object',
129
+ required: ['action', 'conversationId', 'contactUserId'],
130
+ additionalProperties: false,
131
+ properties: {
132
+ action: { const: 'share_contact' },
133
+ conversationId: NON_EMPTY_ID,
134
+ contactUserId: NON_EMPTY_ID,
135
+ text: OPTIONAL_CAPTION,
136
+ messageId: NON_EMPTY_ID,
137
+ },
138
+ },
139
+ {
140
+ type: 'object',
141
+ required: ['action', 'conversationId', 'userId', 'operation'],
142
+ additionalProperties: false,
143
+ properties: {
144
+ action: { const: 'manage_group_members' },
145
+ conversationId: NON_EMPTY_ID,
146
+ userId: NON_EMPTY_ID,
147
+ operation: { enum: ['add', 'remove'] },
148
+ },
149
+ },
101
150
  ],
102
151
  },
103
152
  };
@@ -115,6 +164,36 @@ function hasOnlyKeys(record, allowed) {
115
164
  export function parseCommunicateToolInput(value) {
116
165
  if (!isRecord(value))
117
166
  throw new Error('communicate arguments must be an object');
167
+ if (value.action === 'discover_agents') {
168
+ if (!hasOnlyKeys(value, new Set(['action', 'query', 'limit', 'cursor']))) {
169
+ throw new Error('discover_agents contains unsupported fields');
170
+ }
171
+ if (value.query !== undefined && typeof value.query !== 'string') {
172
+ throw new Error('communicate.query must be a string');
173
+ }
174
+ if (typeof value.query === 'string'
175
+ && value.query.length > MAX_DISCOVER_AGENTS_QUERY_LENGTH) {
176
+ throw new Error(`communicate.query must be at most ${MAX_DISCOVER_AGENTS_QUERY_LENGTH} characters`);
177
+ }
178
+ if (value.limit !== undefined
179
+ && (!Number.isInteger(value.limit)
180
+ || value.limit < 1
181
+ || value.limit > CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS)) {
182
+ throw new Error(`communicate.limit must be an integer from 1 to ${CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS}`);
183
+ }
184
+ if (value.cursor !== undefined
185
+ && (typeof value.cursor !== 'string' || value.cursor.length === 0)) {
186
+ throw new Error('communicate.cursor must be a non-empty string');
187
+ }
188
+ return {
189
+ action: 'discover_agents',
190
+ ...(typeof value.query === 'string' ? { query: value.query } : {}),
191
+ limit: typeof value.limit === 'number'
192
+ ? value.limit
193
+ : CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS,
194
+ ...(typeof value.cursor === 'string' ? { cursor: value.cursor } : {}),
195
+ };
196
+ }
118
197
  if (value.action === 'create_group') {
119
198
  if (!hasOnlyKeys(value, new Set(['action', 'name', 'memberIds']))) {
120
199
  throw new Error('create_group contains unsupported fields');
@@ -163,6 +242,64 @@ export function parseCommunicateToolInput(value) {
163
242
  ...(value.text !== undefined ? { text: value.text } : {}),
164
243
  };
165
244
  }
245
+ if (value.action === 'share_contact') {
246
+ if (!hasOnlyKeys(value, new Set([
247
+ 'action',
248
+ 'conversationId',
249
+ 'contactUserId',
250
+ 'text',
251
+ 'messageId',
252
+ ]))) {
253
+ throw new Error('share_contact contains unsupported fields');
254
+ }
255
+ const conversationId = readRequiredString(value.conversationId);
256
+ const contactUserId = readRequiredString(value.contactUserId);
257
+ const messageId = value.messageId === undefined
258
+ ? undefined
259
+ : readRequiredString(value.messageId);
260
+ if (!conversationId)
261
+ throw new Error('communicate.conversationId is required');
262
+ if (!contactUserId)
263
+ throw new Error('communicate.contactUserId is required');
264
+ if (value.text !== undefined && typeof value.text !== 'string') {
265
+ throw new Error('communicate.text must be a string');
266
+ }
267
+ if (value.messageId !== undefined && !messageId) {
268
+ throw new Error('communicate.messageId must be a non-empty string');
269
+ }
270
+ return {
271
+ action: 'share_contact',
272
+ conversationId,
273
+ contactUserId,
274
+ ...(value.text !== undefined ? { text: value.text } : {}),
275
+ ...(messageId ? { messageId } : {}),
276
+ };
277
+ }
278
+ if (value.action === 'manage_group_members') {
279
+ if (!hasOnlyKeys(value, new Set([
280
+ 'action',
281
+ 'conversationId',
282
+ 'userId',
283
+ 'operation',
284
+ ]))) {
285
+ throw new Error('manage_group_members contains unsupported fields');
286
+ }
287
+ const conversationId = readRequiredString(value.conversationId);
288
+ const userId = readRequiredString(value.userId);
289
+ if (!conversationId)
290
+ throw new Error('communicate.conversationId is required');
291
+ if (!userId)
292
+ throw new Error('communicate.userId is required');
293
+ if (value.operation !== 'add' && value.operation !== 'remove') {
294
+ throw new Error('communicate.operation must be add or remove');
295
+ }
296
+ return {
297
+ action: 'manage_group_members',
298
+ conversationId,
299
+ userId,
300
+ operation: value.operation,
301
+ };
302
+ }
166
303
  const text = readRequiredString(value.text);
167
304
  const messageId = value.messageId === undefined
168
305
  ? undefined
@@ -207,16 +344,21 @@ export function parseCommunicateToolInput(value) {
207
344
  throw new Error('communicate.action is invalid');
208
345
  }
209
346
  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;
347
+ let id;
348
+ if (result.status === 'messaged'
349
+ || result.status === 'forwarded'
350
+ || result.status === 'shared') {
351
+ id = result.messageId;
352
+ }
353
+ else if (result.status === 'added' || result.status === 'removed') {
354
+ id = result.userId;
355
+ }
356
+ else if (result.status === 'created') {
357
+ id = result.conversationId;
358
+ }
359
+ else if (result.status === 'requested' || result.status === 'pending') {
360
+ id = result.requestId;
361
+ }
220
362
  return {
221
363
  content: [{
222
364
  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
- export { CANON_COMMUNICATE_TOOL_NAME, canonCommunicateToolDefinition, createCanonCommunicationBinding, executeCanonCommunicateTool, parseCommunicateToolInput, type CanonCommunicationBinding, } from './communication-tool.js';
3
+ export { CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS, 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
- export { CANON_COMMUNICATE_TOOL_NAME, canonCommunicateToolDefinition, createCanonCommunicationBinding, executeCanonCommunicateTool, parseCommunicateToolInput, } from './communication-tool.js';
3
+ export { CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS, 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.9.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.1.0",
45
+ "@canonmsg/rich-cards": "^0.10.4",
45
46
  "@modelcontextprotocol/sdk": "^1.30.0"
46
47
  },
47
48
  "devDependencies": {