@canonmsg/agent-tools 0.3.4 → 0.5.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 +2 -2
- package/dist/verb-mcp.d.ts +10 -2
- package/dist/verb-mcp.js +15 -1
- package/dist/verb-tools.d.ts +2 -0
- package/dist/verb-tools.js +32 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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
|
|
5
|
+
This is the public tool surface of Canon's verb layer. All seventeen `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`, `no_reply` — are projected here as JSON-Schema tool definitions and dispatched over one endpoint: `POST /agent/verbs/:verb`.
|
|
6
6
|
|
|
7
7
|
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
8
|
|
|
@@ -34,7 +34,7 @@ const result = await executeCanonVerbTool(client, 'send_to', {
|
|
|
34
34
|
|
|
35
35
|
A bare `CanonClient` targets production; pass a base URL from `resolveCanonRuntimeConnection({ environmentId: 'canon-dev-v1' })` to point at dev.
|
|
36
36
|
|
|
37
|
-
For an MCP mount, `createCanonVerbMcpServer(getClient, getContext?)` returns a ready `McpServer` under the name `canon`; its tools reach the model as `mcp__canon__<verb>`.
|
|
37
|
+
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.
|
|
38
38
|
|
|
39
39
|
## Notes
|
|
40
40
|
|
package/dist/verb-mcp.d.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* Detached approvals still return pending immediately.
|
|
20
20
|
*/
|
|
21
21
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
22
|
-
import type { CanonClient } from '@canonmsg/core';
|
|
22
|
+
import type { CanonClient, CanonVerbName } from '@canonmsg/core';
|
|
23
23
|
import { type VerbExecutionContext } from './verb-tools.js';
|
|
24
24
|
/** MCP server name — verbs appear to the model as `mcp__canon__<verb>`. */
|
|
25
25
|
export declare const CANON_VERB_MCP_SERVER_NAME = "canon";
|
|
@@ -29,4 +29,12 @@ export declare function createCanonVerbMcpServer(getClient: () => CanonClient |
|
|
|
29
29
|
* active conversation, current turn, turn responder). Read at call time —
|
|
30
30
|
* the session object outlives any single turn.
|
|
31
31
|
*/
|
|
32
|
-
getContext?: () => VerbExecutionContext | undefined
|
|
32
|
+
getContext?: () => VerbExecutionContext | undefined,
|
|
33
|
+
/**
|
|
34
|
+
* Observation seam: the only place a host learns that a verb tool actually
|
|
35
|
+
* executed (canUseTool fires before dispatch and can still be denied). It
|
|
36
|
+
* runs BEFORE the POST on purpose — a host flag set here survives an
|
|
37
|
+
* un-deployed server, so `no_reply` degrades to "silent turn, error shown to
|
|
38
|
+
* the model" rather than "silence never happens".
|
|
39
|
+
*/
|
|
40
|
+
onVerbCall?: (verb: CanonVerbName, args: Record<string, unknown>, context: VerbExecutionContext | undefined) => void): McpServer;
|
package/dist/verb-mcp.js
CHANGED
|
@@ -29,7 +29,15 @@ export function createCanonVerbMcpServer(getClient,
|
|
|
29
29
|
* active conversation, current turn, turn responder). Read at call time —
|
|
30
30
|
* the session object outlives any single turn.
|
|
31
31
|
*/
|
|
32
|
-
getContext
|
|
32
|
+
getContext,
|
|
33
|
+
/**
|
|
34
|
+
* Observation seam: the only place a host learns that a verb tool actually
|
|
35
|
+
* executed (canUseTool fires before dispatch and can still be denied). It
|
|
36
|
+
* runs BEFORE the POST on purpose — a host flag set here survives an
|
|
37
|
+
* un-deployed server, so `no_reply` degrades to "silent turn, error shown to
|
|
38
|
+
* the model" rather than "silence never happens".
|
|
39
|
+
*/
|
|
40
|
+
onVerbCall) {
|
|
33
41
|
// The Agent SDK's mcpServers option expects the high-level McpServer class,
|
|
34
42
|
// but its registerTool API wants Zod shapes — so the contract's JSON-Schema
|
|
35
43
|
// projections are installed directly on the underlying protocol server
|
|
@@ -66,6 +74,12 @@ getContext) {
|
|
|
66
74
|
: {};
|
|
67
75
|
const verbArgs = name === 'send_to' ? stampSendToTurnComplete(rawArgs) : rawArgs;
|
|
68
76
|
const context = getContext?.();
|
|
77
|
+
try {
|
|
78
|
+
onVerbCall?.(name, verbArgs, context);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// A host bookkeeping bug must never break tool dispatch.
|
|
82
|
+
}
|
|
69
83
|
const result = await executeCanonVerbTool(client, name, verbArgs, {
|
|
70
84
|
waitForResult: true,
|
|
71
85
|
signal: extra?.signal,
|
package/dist/verb-tools.d.ts
CHANGED
|
@@ -77,6 +77,8 @@ export interface VerbExecutionContext {
|
|
|
77
77
|
turnId?: string;
|
|
78
78
|
/** Default responder (e.g. the author of the turn being served). */
|
|
79
79
|
responseUserId?: string;
|
|
80
|
+
/** Canon message id of the inbound message that triggered this turn. */
|
|
81
|
+
sourceMessageId?: string;
|
|
80
82
|
}
|
|
81
83
|
export interface ExecuteVerbToolOptions {
|
|
82
84
|
/**
|
package/dist/verb-tools.js
CHANGED
|
@@ -53,6 +53,10 @@ const CANON_VERB_TOOL_DESCRIPTIONS = {
|
|
|
53
53
|
list_contacts: 'List your Canon contacts.',
|
|
54
54
|
list_contact_requests: 'List pending inbound contact requests (read-only awareness).',
|
|
55
55
|
list_conversations: 'List your Canon conversations (optionally limited).',
|
|
56
|
+
no_reply: 'End your turn without posting anything to the conversation. Use it in '
|
|
57
|
+
+ 'groups when you have nothing to add — no message is created, so no '
|
|
58
|
+
+ 'other member or agent is triggered. Optional private reason (logged, '
|
|
59
|
+
+ 'never shown). After calling this, produce no further text.',
|
|
56
60
|
};
|
|
57
61
|
/**
|
|
58
62
|
* Posture-specific completion sentences for the blocking interactive verbs.
|
|
@@ -90,6 +94,15 @@ export function canonVerbToolDefinitions(options = {}) {
|
|
|
90
94
|
delete properties[field];
|
|
91
95
|
inputSchema = { ...inputSchema, properties };
|
|
92
96
|
}
|
|
97
|
+
if (verb === 'no_reply') {
|
|
98
|
+
// Binding-owned like turnId: injected from turn context, never asked
|
|
99
|
+
// of the model — a guessed id is worse than none.
|
|
100
|
+
const properties = {
|
|
101
|
+
...(inputSchema.properties ?? {}),
|
|
102
|
+
};
|
|
103
|
+
delete properties.messageId;
|
|
104
|
+
inputSchema = { ...inputSchema, properties };
|
|
105
|
+
}
|
|
93
106
|
// The contract marks conversationId optional because bindings default it
|
|
94
107
|
// to the active conversation — a projection without one must require it
|
|
95
108
|
// from the model instead. Context-bound fields are either injected by
|
|
@@ -167,6 +180,25 @@ const DEFAULT_INTERACTION_TIMEOUT_MS = 5 * 60 * 1000;
|
|
|
167
180
|
* without a conversation.
|
|
168
181
|
*/
|
|
169
182
|
export function normalizeVerbToolArgs(verb, args, context, now = Date.now()) {
|
|
183
|
+
if (verb === 'no_reply') {
|
|
184
|
+
const completed = { ...args };
|
|
185
|
+
// messageId is binding-owned (like turnId on interaction verbs): the
|
|
186
|
+
// model never knows the triggering id more reliably than the binding,
|
|
187
|
+
// and a wrong id misattributes the silence record.
|
|
188
|
+
delete completed.messageId;
|
|
189
|
+
if (typeof context?.sourceMessageId === 'string' && context.sourceMessageId) {
|
|
190
|
+
completed.messageId = context.sourceMessageId;
|
|
191
|
+
}
|
|
192
|
+
// Silence carries a conversation for the server's durable silence marker
|
|
193
|
+
// (/runtime-silence/{convoId}/{agentId}), so it defaults like an
|
|
194
|
+
// interaction verb but never FAILS without one — a turn can decline to
|
|
195
|
+
// reply from a binding that has no active conversation.
|
|
196
|
+
if ((typeof completed.conversationId !== 'string' || !completed.conversationId)
|
|
197
|
+
&& typeof context?.conversationId === 'string' && context.conversationId) {
|
|
198
|
+
completed.conversationId = context.conversationId;
|
|
199
|
+
}
|
|
200
|
+
return { args: completed };
|
|
201
|
+
}
|
|
170
202
|
if (!INTERACTION_VERBS.has(verb))
|
|
171
203
|
return { args };
|
|
172
204
|
const completed = { ...args };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/agent-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@canonmsg/core": "^
|
|
44
|
-
"@canonmsg/rich-cards": "^0.9.
|
|
43
|
+
"@canonmsg/core": "^10.2.0",
|
|
44
|
+
"@canonmsg/rich-cards": "^0.9.1",
|
|
45
45
|
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|