@canonmsg/agent-tools 0.5.2 → 0.6.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 +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/owner-bound-communication.d.ts +36 -0
- package/dist/owner-bound-communication.js +163 -0
- package/dist/verb-mcp.d.ts +17 -1
- package/dist/verb-mcp.js +83 -5
- package/dist/verb-tools.d.ts +12 -0
- package/dist/verb-tools.js +33 -1
- package/package.json +4 -4
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 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`.
|
|
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
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
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, } from './verb-mcp.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';
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { CANON_TOOL_VERBS, canonVerbToolDefinitions, executeCanonVerbTool, isCanonToolVerb, normalizeVerbToolArgs, stampSendToTurnComplete, } from './verb-tools.js';
|
|
2
|
-
export { CANON_VERB_MCP_SERVER_NAME, createCanonVerbMcpServer, } from './verb-mcp.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';
|
|
@@ -0,0 +1,36 @@
|
|
|
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;
|
|
@@ -0,0 +1,163 @@
|
|
|
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
|
+
}
|
package/dist/verb-mcp.d.ts
CHANGED
|
@@ -21,8 +21,18 @@
|
|
|
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';
|
|
24
25
|
/** MCP server name — verbs appear to the model as `mcp__canon__<verb>`. */
|
|
25
26
|
export declare const CANON_VERB_MCP_SERVER_NAME = "canon";
|
|
27
|
+
export interface CanonVerbMcpServerOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Replace the five communication verbs with the strict owner-bound
|
|
30
|
+
* projection. Other canonical verbs retain their existing scoped behavior.
|
|
31
|
+
*/
|
|
32
|
+
ownerBoundCommunication?: {
|
|
33
|
+
getContext: () => OwnerBoundCanonTurnContext | null | undefined;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
26
36
|
export declare function createCanonVerbMcpServer(getClient: () => CanonClient | null,
|
|
27
37
|
/**
|
|
28
38
|
* Lazy per-call execution context (host sessions are conversation-scoped:
|
|
@@ -37,4 +47,10 @@ getContext?: () => VerbExecutionContext | undefined,
|
|
|
37
47
|
* un-deployed server, so `no_reply` degrades to "silent turn, error shown to
|
|
38
48
|
* the model" rather than "silence never happens".
|
|
39
49
|
*/
|
|
40
|
-
onVerbCall?: (verb: CanonVerbName, args: Record<string, unknown>, context: VerbExecutionContext | undefined) => void): McpServer;
|
|
50
|
+
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
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
22
22
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
23
23
|
import { canonVerbToolDefinitions, executeCanonVerbTool, isCanonToolVerb, stampSendToTurnComplete, } from './verb-tools.js';
|
|
24
|
+
import { OWNER_BOUND_CANON_COMMUNICATION_VERBS, createOwnerBoundCanonCommunicationBinding, } from './owner-bound-communication.js';
|
|
24
25
|
/** MCP server name — verbs appear to the model as `mcp__canon__<verb>`. */
|
|
25
26
|
export const CANON_VERB_MCP_SERVER_NAME = 'canon';
|
|
26
27
|
export function createCanonVerbMcpServer(getClient,
|
|
@@ -37,7 +38,7 @@ getContext,
|
|
|
37
38
|
* un-deployed server, so `no_reply` degrades to "silent turn, error shown to
|
|
38
39
|
* the model" rather than "silence never happens".
|
|
39
40
|
*/
|
|
40
|
-
onVerbCall) {
|
|
41
|
+
onVerbCall, options = {}) {
|
|
41
42
|
// The Agent SDK's mcpServers option expects the high-level McpServer class,
|
|
42
43
|
// but its registerTool API wants Zod shapes — so the contract's JSON-Schema
|
|
43
44
|
// projections are installed directly on the underlying protocol server
|
|
@@ -48,12 +49,24 @@ onVerbCall) {
|
|
|
48
49
|
// The projection must tell the model what the dispatch below actually
|
|
49
50
|
// does: waiting posture (waitForResult: true), and conversation-scoped
|
|
50
51
|
// exactly when the binding supplies a context.
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
const ownerBinding = options.ownerBoundCommunication
|
|
53
|
+
? createOwnerBoundCanonCommunicationBinding()
|
|
54
|
+
: null;
|
|
55
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
56
|
+
const canonical = canonVerbToolDefinitions({
|
|
53
57
|
interaction: 'waiting',
|
|
54
58
|
conversationScoped: Boolean(getContext),
|
|
55
|
-
})
|
|
56
|
-
|
|
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
|
+
});
|
|
57
70
|
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
58
71
|
const client = getClient();
|
|
59
72
|
if (!client) {
|
|
@@ -72,6 +85,24 @@ onVerbCall) {
|
|
|
72
85
|
const rawArgs = request.params.arguments && typeof request.params.arguments === 'object'
|
|
73
86
|
? { ...request.params.arguments }
|
|
74
87
|
: {};
|
|
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
|
+
}
|
|
75
106
|
const verbArgs = name === 'send_to' ? stampSendToTurnComplete(rawArgs) : rawArgs;
|
|
76
107
|
const context = getContext?.();
|
|
77
108
|
try {
|
|
@@ -92,3 +123,50 @@ onVerbCall) {
|
|
|
92
123
|
});
|
|
93
124
|
return mcpServer;
|
|
94
125
|
}
|
|
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
|
+
}
|
package/dist/verb-tools.d.ts
CHANGED
|
@@ -79,6 +79,18 @@ export interface VerbExecutionContext {
|
|
|
79
79
|
responseUserId?: string;
|
|
80
80
|
/** Canon message id of the inbound message that triggered this turn. */
|
|
81
81
|
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
|
+
};
|
|
82
94
|
}
|
|
83
95
|
export interface ExecuteVerbToolOptions {
|
|
84
96
|
/**
|
package/dist/verb-tools.js
CHANGED
|
@@ -51,8 +51,9 @@ const CANON_VERB_TOOL_DESCRIPTIONS = {
|
|
|
51
51
|
remove_member: 'Remove a member from a Canon group (requires owner/admin role).',
|
|
52
52
|
leave_conversation: 'Leave a Canon group conversation.',
|
|
53
53
|
list_contacts: 'List your Canon contacts.',
|
|
54
|
-
list_contact_requests: 'List pending inbound
|
|
54
|
+
list_contact_requests: 'List contact requests. Defaults to pending inbound; use outbound with includeResolved for reconnect recovery.',
|
|
55
55
|
list_conversations: 'List your Canon conversations (optionally limited).',
|
|
56
|
+
cancel_contact_request: 'Cancel your own pending direct-message contact request.',
|
|
56
57
|
no_reply: 'End your turn without posting anything to the conversation. Use it in '
|
|
57
58
|
+ 'groups when you have nothing to add — no message is created, so no '
|
|
58
59
|
+ 'other member or agent is triggered. Optional private reason (logged, '
|
|
@@ -180,6 +181,34 @@ const DEFAULT_INTERACTION_TIMEOUT_MS = 5 * 60 * 1000;
|
|
|
180
181
|
* without a conversation.
|
|
181
182
|
*/
|
|
182
183
|
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
|
+
}
|
|
183
212
|
if (verb === 'no_reply') {
|
|
184
213
|
const completed = { ...args };
|
|
185
214
|
// messageId is binding-owned (like turnId on interaction verbs): the
|
|
@@ -260,6 +289,9 @@ export function normalizeVerbToolArgs(verb, args, context, now = Date.now()) {
|
|
|
260
289
|
}
|
|
261
290
|
return { args: completed };
|
|
262
291
|
}
|
|
292
|
+
function isRecord(value) {
|
|
293
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
294
|
+
}
|
|
263
295
|
const DEFAULT_POLL_MS = 1000;
|
|
264
296
|
const DEFAULT_MAX_WAIT_MS = 30 * 60 * 1000;
|
|
265
297
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/agent-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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,9 +40,9 @@
|
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@canonmsg/core": "^10.
|
|
44
|
-
"@canonmsg/rich-cards": "^0.10.
|
|
45
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
43
|
+
"@canonmsg/core": "^10.7.0",
|
|
44
|
+
"@canonmsg/rich-cards": "^0.10.2",
|
|
45
|
+
"@modelcontextprotocol/sdk": "^1.30.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/node": "^22.0.0",
|